@inixiative/foundry-core 0.5.1 → 0.5.3
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/package.json +1 -1
- package/src/adapters/markdown-docs.ts +11 -2
- package/src/adapters/markdown-hierarchy.ts +18 -4
- package/src/context-layer.ts +77 -0
- package/src/feedback/correction.ts +8 -1
- package/src/feedback/experiment-gate.ts +89 -0
- package/src/feedback/review-findings.ts +237 -0
- package/src/feedback/warden-labels.ts +232 -0
- package/src/feedback/warden-prefilter.ts +196 -0
- package/src/feedback/warden-probes.ts +580 -0
- package/src/index.ts +96 -0
- package/src/messages.ts +8 -2
- package/src/native-evidence.ts +24 -0
- package/src/thread.ts +39 -0
- package/src/types.ts +12 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { WardenProbe } from './warden-probes';
|
|
2
|
+
|
|
3
|
+
/** One Warden of the roster as the labeler sees it. */
|
|
4
|
+
export interface WardenRosterEntry {
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly domain: string;
|
|
7
|
+
readonly prompt: string;
|
|
8
|
+
readonly enabled: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Whether a Warden had something of its own to add at the moment, and what it should have known. */
|
|
12
|
+
export interface WardenLabel {
|
|
13
|
+
readonly relevant: boolean;
|
|
14
|
+
readonly shouldKnow?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type WardenTriggerKind = 'user-message' | 'agent-action' | 'agent-plan' | 'subagent-spawn';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A labelled moment for the Warden prefilter: which Warden domains the trigger touches. Never commit
|
|
21
|
+
* fixtures: they hold session text and company code excerpts.
|
|
22
|
+
*/
|
|
23
|
+
export interface WardenFixture {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
/** Which context's Wardens may see it (UserEvidence fixtures only ever see the UserEvidence ladder). */
|
|
26
|
+
readonly ctx: string;
|
|
27
|
+
readonly project?: string;
|
|
28
|
+
readonly source: 'git' | 'session' | 'history';
|
|
29
|
+
/** The moment before the mistake, as a Warden would see it. */
|
|
30
|
+
readonly trigger: { readonly kind: WardenTriggerKind; readonly text: string };
|
|
31
|
+
/** The thread or change up to the moment; never what happened next. */
|
|
32
|
+
readonly context: string;
|
|
33
|
+
/** What happened next (the correction, the fix, the review). The labeler sees it; Wardens never do. */
|
|
34
|
+
readonly hidden?: string;
|
|
35
|
+
/** Every Warden of the roster, keyed by agent id. */
|
|
36
|
+
readonly labels: Readonly<Record<string, WardenLabel>>;
|
|
37
|
+
/** The FEEDBACK-LOG rule numbers the moment's mistake repeats, when any. */
|
|
38
|
+
readonly feedbackRules?: readonly number[];
|
|
39
|
+
readonly provenance: Readonly<Record<string, string | number | null>>;
|
|
40
|
+
readonly labeler?: { readonly model: string; readonly run: string; readonly promptHash: string };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type UnlabelledFixture = Omit<WardenFixture, 'labels' | 'labeler' | 'feedbackRules'> & {
|
|
44
|
+
/** Earlier asks or commits in the same thread, for the labeler to summarize into `context`. */
|
|
45
|
+
readonly precedingRaw?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const WARDEN_LABEL_PROMPT = `You label moments from AI coding sessions and code changes for a reviewer-routing experiment.
|
|
49
|
+
|
|
50
|
+
A roster of Wardens advises a coding agent. Each Warden owns one narrow domain (given below with its charter). At each moment (a user's message to the agent, or an agent's action or plan), a router must decide which Wardens to wake. You produce the ground truth for that router.
|
|
51
|
+
|
|
52
|
+
For each moment you get: the thread context, the trigger (the moment itself), and WHAT HAPPENED NEXT (the person's correction, a later fix, a review comment, or nothing notable). Use what happened next to understand what was wrong or what was needed, but judge each Warden by the moment: would a competent Warden of that domain, seeing the trigger and context, have had something true, specific and useful to say inside its own domain? That includes the Warden that would have prevented the mistake, and any other Warden whose domain the moment genuinely touches (for example a schema change touches Schema even when the correction was about reuse). It does NOT include a Warden that could only say something generic, or whose domain the moment merely mentions in passing.
|
|
53
|
+
|
|
54
|
+
Most Wardens are irrelevant to most moments. A moment where nothing went wrong can still be relevant to a Warden (a plan heading to merge without the full check touches Ship), but plain chatter, a status question, or an answer to the agent's question usually touches none.
|
|
55
|
+
|
|
56
|
+
For each relevant Warden, write shouldKnow: the specific thing it should have known or said, at most 200 characters, naming the existing primitive, file, symbol, rule or check when there is one (e.g. "Use the existing debounce wrapper in packages/shared/debounce.ts instead of a new timer"). Never quote secrets.
|
|
57
|
+
|
|
58
|
+
Also write context: 1-3 sentences summarizing the thread or change UP TO the moment (what is being worked on, where), without revealing what happened next. And feedbackRules: the numbers of the recurring rules (list given) that the moment's mistake repeats, or [].
|
|
59
|
+
|
|
60
|
+
Respond with JSON only:
|
|
61
|
+
{ "results": { "<fixture id>": { "context": string, "relevant": { "<domain>": "<shouldKnow>" }, "feedbackRules": number[] } } }
|
|
62
|
+
List only relevant domains under "relevant" (an empty object when none). Use the domain names exactly as given.`;
|
|
63
|
+
|
|
64
|
+
/** The stable start of every label input: the roster and the rule list, so primed sessions reuse it. */
|
|
65
|
+
export function wardenLabelPrefix(
|
|
66
|
+
roster: readonly WardenRosterEntry[],
|
|
67
|
+
rules: readonly { n: number; rule: string; count: string }[],
|
|
68
|
+
): string {
|
|
69
|
+
const wardens = roster.map((w) => `- ${w.domain}: ${w.prompt.replace(/\s+/g, ' ')}`).join('\n');
|
|
70
|
+
const recurring = rules.map((r) => `${r.n}. ${r.rule} (said ${r.count}×)`).join('\n');
|
|
71
|
+
return `## Wardens\n${wardens}\n\n## Recurring rules (FEEDBACK-LOG)\n${recurring}\n\nLabel each moment below.\n\n`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function wardenLabelMessages(
|
|
75
|
+
prefix: string,
|
|
76
|
+
fixtures: readonly UnlabelledFixture[],
|
|
77
|
+
limits: { trigger?: number; context?: number; hidden?: number } = {},
|
|
78
|
+
): { role: 'system' | 'user'; content: string }[] {
|
|
79
|
+
const body = fixtures
|
|
80
|
+
.map(
|
|
81
|
+
(f) =>
|
|
82
|
+
`## ${f.id} (${f.ctx}${f.project ? `/${f.project}` : ''}, ${f.source}, trigger: ${f.trigger.kind})\n### Context\n${
|
|
83
|
+
[f.context, f.precedingRaw]
|
|
84
|
+
.filter(Boolean)
|
|
85
|
+
.join('\n')
|
|
86
|
+
.slice(-(limits.context ?? 1500)) || '(none)'
|
|
87
|
+
}\n\n### Trigger\n${f.trigger.text.slice(-(limits.trigger ?? 3000))}\n\n### What happened next\n${f.hidden?.slice(0, limits.hidden ?? 1500) || '(nothing notable)'}`,
|
|
88
|
+
)
|
|
89
|
+
.join('\n\n');
|
|
90
|
+
return [
|
|
91
|
+
{ role: 'system', content: WARDEN_LABEL_PROMPT },
|
|
92
|
+
{ role: 'user', content: `${prefix}${body}` },
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ParsedWardenLabels {
|
|
97
|
+
readonly context: string;
|
|
98
|
+
readonly labels: Record<string, WardenLabel>;
|
|
99
|
+
readonly feedbackRules: number[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** One answer per fixture id; undefined where the answer is missing or malformed. Unknown domains are dropped. */
|
|
103
|
+
export function parseWardenLabels(
|
|
104
|
+
content: string,
|
|
105
|
+
roster: readonly WardenRosterEntry[],
|
|
106
|
+
ids: readonly string[],
|
|
107
|
+
): (ParsedWardenLabels | undefined)[] {
|
|
108
|
+
const start = content.indexOf('{');
|
|
109
|
+
const end = content.lastIndexOf('}');
|
|
110
|
+
let raw: unknown;
|
|
111
|
+
try {
|
|
112
|
+
raw = JSON.parse(content.slice(start, end + 1));
|
|
113
|
+
} catch {
|
|
114
|
+
return ids.map(() => undefined);
|
|
115
|
+
}
|
|
116
|
+
const results = (raw as { results?: Record<string, unknown> } | null)?.results ?? {};
|
|
117
|
+
const byDomain = new Map(roster.map((w) => [w.domain, w.id]));
|
|
118
|
+
return ids.map((id) => {
|
|
119
|
+
const item = results[id] as
|
|
120
|
+
| { context?: unknown; relevant?: unknown; feedbackRules?: unknown }
|
|
121
|
+
| undefined;
|
|
122
|
+
if (!item || typeof item.relevant !== 'object' || item.relevant === null) return undefined;
|
|
123
|
+
const relevant = item.relevant as Record<string, unknown>;
|
|
124
|
+
const labels: Record<string, WardenLabel> = {};
|
|
125
|
+
for (const w of roster) labels[w.id] = { relevant: false };
|
|
126
|
+
for (const [domain, know] of Object.entries(relevant)) {
|
|
127
|
+
const wardenId = byDomain.get(domain);
|
|
128
|
+
if (!wardenId) continue;
|
|
129
|
+
labels[wardenId] = {
|
|
130
|
+
relevant: true,
|
|
131
|
+
...(typeof know === 'string' && know.trim()
|
|
132
|
+
? { shouldKnow: know.trim().slice(0, 300) }
|
|
133
|
+
: {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
context: typeof item.context === 'string' ? item.context.trim() : '',
|
|
138
|
+
labels,
|
|
139
|
+
feedbackRules: Array.isArray(item.feedbackRules)
|
|
140
|
+
? item.feedbackRules.filter((n): n is number => Number.isInteger(n))
|
|
141
|
+
: [],
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface DomainAgreement {
|
|
147
|
+
readonly wardenId: string;
|
|
148
|
+
/** Fixtures both sides labelled. */
|
|
149
|
+
readonly n: number;
|
|
150
|
+
readonly agree: number;
|
|
151
|
+
readonly positivesA: number;
|
|
152
|
+
readonly positivesB: number;
|
|
153
|
+
readonly bothPositive: number;
|
|
154
|
+
/** Cohen's kappa; null when chance agreement is total (one class on both sides). */
|
|
155
|
+
readonly kappa: number | null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Per-Warden agreement between two labelings of the same fixtures, plus the pooled figures. */
|
|
159
|
+
export function labelAgreement(
|
|
160
|
+
a: ReadonlyMap<string, Readonly<Record<string, WardenLabel>>>,
|
|
161
|
+
b: ReadonlyMap<string, Readonly<Record<string, WardenLabel>>>,
|
|
162
|
+
wardenIds: readonly string[],
|
|
163
|
+
): {
|
|
164
|
+
perWarden: DomainAgreement[];
|
|
165
|
+
pooled: DomainAgreement;
|
|
166
|
+
exactFixtures: number;
|
|
167
|
+
fixtures: number;
|
|
168
|
+
} {
|
|
169
|
+
const shared = [...a.keys()].filter((id) => b.has(id));
|
|
170
|
+
const tally = (wardenId: string, ids: readonly string[], wardens: readonly string[]) => {
|
|
171
|
+
let n = 0;
|
|
172
|
+
let agree = 0;
|
|
173
|
+
let positivesA = 0;
|
|
174
|
+
let positivesB = 0;
|
|
175
|
+
let bothPositive = 0;
|
|
176
|
+
for (const id of ids)
|
|
177
|
+
for (const w of wardens) {
|
|
178
|
+
const x = a.get(id)?.[w]?.relevant ?? false;
|
|
179
|
+
const y = b.get(id)?.[w]?.relevant ?? false;
|
|
180
|
+
n++;
|
|
181
|
+
if (x === y) agree++;
|
|
182
|
+
if (x) positivesA++;
|
|
183
|
+
if (y) positivesB++;
|
|
184
|
+
if (x && y) bothPositive++;
|
|
185
|
+
}
|
|
186
|
+
const observed = n ? agree / n : 0;
|
|
187
|
+
const chance = n
|
|
188
|
+
? (positivesA / n) * (positivesB / n) + (1 - positivesA / n) * (1 - positivesB / n)
|
|
189
|
+
: 1;
|
|
190
|
+
const kappa =
|
|
191
|
+
chance >= 1 ? null : Math.round(((observed - chance) / (1 - chance)) * 1000) / 1000;
|
|
192
|
+
return { wardenId, n, agree, positivesA, positivesB, bothPositive, kappa };
|
|
193
|
+
};
|
|
194
|
+
const exactFixtures = shared.filter((id) =>
|
|
195
|
+
wardenIds.every(
|
|
196
|
+
(w) => (a.get(id)?.[w]?.relevant ?? false) === (b.get(id)?.[w]?.relevant ?? false),
|
|
197
|
+
),
|
|
198
|
+
).length;
|
|
199
|
+
return {
|
|
200
|
+
perWarden: wardenIds.map((w) => tally(w, shared, [w])),
|
|
201
|
+
pooled: tally('*', shared, wardenIds),
|
|
202
|
+
exactFixtures,
|
|
203
|
+
fixtures: shared.length,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The charter eval's probe shape, so `warden-charter.ts` runs on fixtures too. */
|
|
208
|
+
export function fixtureProbe(
|
|
209
|
+
fixture: WardenFixture,
|
|
210
|
+
domainOf: (wardenId: string) => string,
|
|
211
|
+
): WardenProbe {
|
|
212
|
+
const relevant = Object.entries(fixture.labels).filter(([, l]) => l.relevant);
|
|
213
|
+
return {
|
|
214
|
+
id: fixture.id,
|
|
215
|
+
kind: relevant.length ? 'positive' : 'negative',
|
|
216
|
+
ctx: fixture.ctx,
|
|
217
|
+
...(fixture.project ? { project: fixture.project } : {}),
|
|
218
|
+
// A user-message fixture is asked at the user-message trigger; the others are the agent's turn.
|
|
219
|
+
...(fixture.trigger.kind === 'user-message'
|
|
220
|
+
? { moment: '', ask: fixture.trigger.text }
|
|
221
|
+
: { moment: fixture.trigger.text }),
|
|
222
|
+
...(relevant.length
|
|
223
|
+
? {
|
|
224
|
+
gold: {
|
|
225
|
+
domains: relevant.map(([id]) => domainOf(id)),
|
|
226
|
+
statements: relevant.map(([, l]) => l.shouldKnow).filter((s): s is string => !!s),
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
: {}),
|
|
230
|
+
...(fixture.hidden ? { hidden: fixture.hidden } : {}),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// The Warden pre-filter: one cheap local yes/no per Warden before it is asked. Data and pure
|
|
2
|
+
// functions only, so Oracle scores runs the same way; Foundry's provider makes the call. It fails
|
|
3
|
+
// open: a Warden is skipped only on a confident no, and anything else (an unsure score, an error, no
|
|
4
|
+
// answer) asks the Warden as before.
|
|
5
|
+
|
|
6
|
+
import type { WardenTriggerKind } from './warden-labels';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Where a moment arrives: before (the user's message), after (the agent's actions of a turn or its
|
|
10
|
+
* plan, and its finished turn), or at a handoff to a subagent. The labelled moments are
|
|
11
|
+
* `WardenFixture`s (`warden-labels`).
|
|
12
|
+
*/
|
|
13
|
+
export type PrefilterTrigger = WardenTriggerKind | 'post-turn';
|
|
14
|
+
|
|
15
|
+
/** One (moment, Warden) pair as a run scored it. */
|
|
16
|
+
export interface PrefilterPair {
|
|
17
|
+
readonly momentId: string;
|
|
18
|
+
readonly wardenId: string;
|
|
19
|
+
/** Labelled relevance; undefined for an unlabelled pair, which still counts toward the skip rate. */
|
|
20
|
+
readonly relevant?: boolean;
|
|
21
|
+
/** P(the moment is in this Warden's domain); undefined when the call failed (the Warden is asked). */
|
|
22
|
+
readonly score?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Fail open: skip only when the score is a confident no. */
|
|
26
|
+
export function prefilterSkips(score: number | undefined, skipBelow: number): boolean {
|
|
27
|
+
return score !== undefined && score < skipBelow;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PrefilterPoint {
|
|
31
|
+
readonly threshold: number;
|
|
32
|
+
/** Share of all pairs skipped: Warden calls saved. */
|
|
33
|
+
readonly skipRate: number;
|
|
34
|
+
/** Share of relevant pairs still asked. */
|
|
35
|
+
readonly recall: number | null;
|
|
36
|
+
/** Share of labelled not-relevant pairs skipped. */
|
|
37
|
+
readonly correctSkips: number | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PrefilterScore {
|
|
41
|
+
readonly pairs: number;
|
|
42
|
+
readonly relevant: number;
|
|
43
|
+
readonly labelledIrrelevant: number;
|
|
44
|
+
readonly errors: number;
|
|
45
|
+
/** Ranking quality over labelled pairs (relevant against labelled not relevant). */
|
|
46
|
+
readonly auroc: number | null;
|
|
47
|
+
/** Recall when the threshold skips this share of all pairs. */
|
|
48
|
+
readonly recallAtSkip: Readonly<Record<string, number | null>>;
|
|
49
|
+
/** The share of pairs skipped while still asking this share of relevant pairs. */
|
|
50
|
+
readonly skipAtRecall: Readonly<Record<string, { threshold: number; skipRate: number } | null>>;
|
|
51
|
+
readonly sweep: readonly PrefilterPoint[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const round = (n: number) => Math.round(n * 1000) / 1000;
|
|
55
|
+
const ratio = (n: number, d: number) => (d ? round(n / d) : null);
|
|
56
|
+
|
|
57
|
+
export const PREFILTER_SWEEP = [
|
|
58
|
+
0.01, 0.02, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9,
|
|
59
|
+
] as const;
|
|
60
|
+
|
|
61
|
+
/** Recall against skip rate, both ways, and the threshold sweep. */
|
|
62
|
+
export function scorePrefilter(
|
|
63
|
+
pairs: readonly PrefilterPair[],
|
|
64
|
+
options: { skipRates?: readonly number[]; recalls?: readonly number[] } = {},
|
|
65
|
+
): PrefilterScore {
|
|
66
|
+
const positives = pairs.filter((p) => p.relevant === true);
|
|
67
|
+
const negatives = pairs.filter((p) => p.relevant === false);
|
|
68
|
+
const at = (threshold: number): PrefilterPoint => {
|
|
69
|
+
const skip = (p: PrefilterPair) => prefilterSkips(p.score, threshold);
|
|
70
|
+
return {
|
|
71
|
+
threshold,
|
|
72
|
+
skipRate: ratio(pairs.filter(skip).length, pairs.length) ?? 0,
|
|
73
|
+
recall: ratio(positives.filter((p) => !skip(p)).length, positives.length),
|
|
74
|
+
correctSkips: ratio(negatives.filter(skip).length, negatives.length),
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
const scoreOf = (p: PrefilterPair) => p.score ?? Number.POSITIVE_INFINITY;
|
|
78
|
+
// Recall at a skip rate: the threshold is the score that skips that share of all pairs.
|
|
79
|
+
const ascending = pairs.map(scoreOf).sort((a, b) => a - b);
|
|
80
|
+
const recallAtSkip: Record<string, number | null> = {};
|
|
81
|
+
for (const rate of options.skipRates ?? [0.3, 0.5, 0.7]) {
|
|
82
|
+
const threshold =
|
|
83
|
+
ascending[Math.min(ascending.length - 1, Math.floor(rate * ascending.length))];
|
|
84
|
+
recallAtSkip[String(rate)] =
|
|
85
|
+
threshold === undefined || !Number.isFinite(threshold) ? null : at(threshold).recall;
|
|
86
|
+
}
|
|
87
|
+
// Skip rate at a recall: the highest threshold that still asks that share of relevant pairs.
|
|
88
|
+
const positiveScores = positives.map(scoreOf).sort((a, b) => a - b);
|
|
89
|
+
const skipAtRecall: Record<string, { threshold: number; skipRate: number } | null> = {};
|
|
90
|
+
for (const target of options.recalls ?? [0.8, 0.9, 0.95]) {
|
|
91
|
+
const threshold = positiveScores[Math.floor((1 - target) * positiveScores.length + 1e-9)];
|
|
92
|
+
skipAtRecall[String(target)] =
|
|
93
|
+
threshold === undefined || !Number.isFinite(threshold)
|
|
94
|
+
? null
|
|
95
|
+
: { threshold: round(threshold), skipRate: at(threshold).skipRate };
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
pairs: pairs.length,
|
|
99
|
+
relevant: positives.length,
|
|
100
|
+
labelledIrrelevant: negatives.length,
|
|
101
|
+
errors: pairs.filter((p) => p.score === undefined).length,
|
|
102
|
+
auroc: auroc(positives, negatives),
|
|
103
|
+
recallAtSkip,
|
|
104
|
+
skipAtRecall,
|
|
105
|
+
sweep: PREFILTER_SWEEP.map(at),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function auroc(positives: readonly PrefilterPair[], negatives: readonly PrefilterPair[]) {
|
|
110
|
+
const pos = positives.flatMap((p) => (p.score === undefined ? [] : [p.score]));
|
|
111
|
+
const neg = negatives.flatMap((p) => (p.score === undefined ? [] : [p.score]));
|
|
112
|
+
if (!pos.length || !neg.length) return null;
|
|
113
|
+
let wins = 0;
|
|
114
|
+
for (const a of pos) for (const b of neg) wins += a > b ? 1 : a === b ? 0.5 : 0;
|
|
115
|
+
return round(wins / (pos.length * neg.length));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The same pair scored N times: the share whose skip/ask decision never changed at the threshold,
|
|
120
|
+
* and the mean spread (max − min) of its score.
|
|
121
|
+
*/
|
|
122
|
+
export function prefilterConsistency(
|
|
123
|
+
repeats: ReadonlyMap<string, readonly (number | undefined)[]>,
|
|
124
|
+
skipBelow: number,
|
|
125
|
+
) {
|
|
126
|
+
let same = 0,
|
|
127
|
+
spread = 0,
|
|
128
|
+
n = 0;
|
|
129
|
+
for (const scores of repeats.values()) {
|
|
130
|
+
if (!scores.length) continue;
|
|
131
|
+
n++;
|
|
132
|
+
if (new Set(scores.map((s) => prefilterSkips(s, skipBelow))).size === 1) same++;
|
|
133
|
+
const values = scores.filter((s): s is number => s !== undefined);
|
|
134
|
+
spread += values.length ? Math.max(...values) - Math.min(...values) : 1;
|
|
135
|
+
}
|
|
136
|
+
return { pairs: n, sameDecision: ratio(same, n), meanSpread: n ? round(spread / n) : null };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function percentiles(values: readonly number[]) {
|
|
140
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
141
|
+
const pick = (p: number) =>
|
|
142
|
+
sorted[Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length))] ?? 0;
|
|
143
|
+
return { p50: pick(50), p95: pick(95), max: sorted.at(-1) ?? 0 };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Characters of a Warden's instructions a question carries: the decision model reads 192 tokens of it. */
|
|
147
|
+
export const PREFILTER_DESCRIPTION_CHARS = 480;
|
|
148
|
+
/** Characters of the moment, head and tail, and of the thread context, a state carries. */
|
|
149
|
+
export const PREFILTER_MOMENT_CHARS = 1600;
|
|
150
|
+
export const PREFILTER_CONTEXT_CHARS = 400;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A Warden's domain as the question states it: its instructions without the "You are the … Warden."
|
|
154
|
+
* opener, so the question reads as a topic.
|
|
155
|
+
*/
|
|
156
|
+
export function prefilterDescription(instructions: string): string {
|
|
157
|
+
const text = instructions
|
|
158
|
+
.replace(/\s+/g, ' ')
|
|
159
|
+
.trim()
|
|
160
|
+
.replace(/^You are the [^.]*\.\s*/, '');
|
|
161
|
+
return clip(text, PREFILTER_DESCRIPTION_CHARS);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The System One yes/no asked per Warden, as a topic ("Is this about …"). On a small hand-checked
|
|
166
|
+
* grid it separated in-domain moments far better than asking about "a reviewer's domain".
|
|
167
|
+
*/
|
|
168
|
+
export function prefilterQuestion(description: string) {
|
|
169
|
+
return { type: 'noul' as const, instructions: `Is this about ${description}` };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The System One state for a moment: a bounded view of what the thread holds, then the moment. */
|
|
173
|
+
export function prefilterState(trigger: PrefilterTrigger, text: string, context?: string): string {
|
|
174
|
+
const label = {
|
|
175
|
+
'user-message': "The user's message",
|
|
176
|
+
'agent-action': "The agent's actions this turn",
|
|
177
|
+
'agent-plan': "The agent's plan",
|
|
178
|
+
'subagent-spawn': 'The task the agent is handing to a subagent',
|
|
179
|
+
'post-turn': "The agent's finished turn",
|
|
180
|
+
}[trigger];
|
|
181
|
+
const moment = window(text.trim(), PREFILTER_MOMENT_CHARS);
|
|
182
|
+
return context?.trim()
|
|
183
|
+
? `${label}:\n${moment}\n\nThread so far: ${clip(context.trim(), PREFILTER_CONTEXT_CHARS)}`
|
|
184
|
+
: `${label}:\n${moment}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function clip(text: string, max: number): string {
|
|
188
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Head and tail of a long text: an agent turn states its plan first and its result last. */
|
|
192
|
+
function window(text: string, max: number): string {
|
|
193
|
+
if (text.length <= max) return text;
|
|
194
|
+
const half = Math.floor((max - 3) / 2);
|
|
195
|
+
return `${text.slice(0, half)} … ${text.slice(-half)}`;
|
|
196
|
+
}
|