/** * The map half of `/learn`: a model reads one session transcript and says what * it saw. * * This replaces the regex gate that used to decide which user turns were worth * looking at. That gate was a whitelist of imperative words, so a directive * phrased any other way — "we're on bun now", "that's not how our error * handling works" — was not ranked low, it was invisible. Recall was traded for * a token budget, silently and unrecoverably. * * The trade here is explicit instead. Every user turn goes to the model * verbatim; the budget is enforced by chunking and by a session cap the reader * can see, not by a filter they cannot. * * What the model does *not* do here is name or count. It used to emit a label * per occurrence — its own canonical name for what was meant — and the reduce * step grouped on exact label equality. That cannot work from inside one * session: the model is asked to hit a shared vocabulary it has never seen, and * on a real corpus it agreed with itself 3 times out of 188. Naming now happens * once, globally, in `cluster.ts`, where every candidate is visible at the same * time. Counting stays in `reduce.ts`, where it always belonged. * * Leaving labels out also makes the cache model-independent. A cached candidate * used to carry a label frozen at mining time, so changing the `fast` tier * forked the vocabulary permanently: old sessions and new ones named the same * thing differently, and neither side reached the repeat threshold. */ import type { AgentMessage } from "@kolisachint/hoocode-agent-core"; import type { Model } from "@kolisachint/hoocode-ai"; /** What kind of thing the model noticed. */ export type CandidateKind = "directive" | "fix" | "request"; /** * One occurrence, as reported by the model reading a single session. * * Deliberately unnamed. What was *meant* is only decidable against everything * else that was said, and this stage sees one session, so it reports what it * saw and leaves grouping to `cluster.ts`. This is also the shape that goes in * the cache, which is why nothing model-specific may live on it. */ export interface MinedCandidate { kind: CandidateKind; /** Verbatim text from the transcript, so the digest can quote rather than paraphrase. */ text: string; /** Why this is durable, in the model's words. Shown when a proposal is borderline. */ rationale?: string; /** The failing command, for `fix` candidates. */ command?: string; /** Short error excerpt, for `fix` candidates. */ errorExcerpt?: string; /** What was done in between, for `fix` candidates. */ interveningCommands?: string[]; /** Files changed as part of the fix. */ editedFiles?: string[]; } /** A candidate once the global naming pass has decided what to call it. */ export interface LabelledCandidate extends MinedCandidate { /** Canonical slug for what was meant. The clustering key. */ label: string; } /** A session reduced to what the miner needs: identity, time, and rendered text. */ export interface MinableSession { id: string; timestamp: string; entries: Array<{ type: string; message?: AgentMessage; }>; } /** * Mines one session. Injectable so the reduce path can be tested without a * model, and so a cached result can stand in for a live call. */ export type Miner = (session: MinableSession, signal?: AbortSignal) => Promise; /** * How much rendered transcript to send per call, given the reading model. * * Only a fraction of the window is used: the instructions, the response, and * tokenizer variance all have to fit alongside, and overshooting costs a * context-overflow error rather than a slightly worse answer. */ export declare function chunkCharsForModel(model: Pick, "contextWindow">): number; /** * Prefix on the message `/learn` injects. Its own digest is persisted like any * other user turn, so without this the next run would mine its own output and * every proposal would compound its own count. */ export declare const LEARN_DIGEST_MARKER = "[learn-digest]"; /** * Literal runs from slash-command bodies, used to recognise a replayed expansion. * * A `user`-type slash command is persisted as an ordinary user message holding * the whole template body, with nothing to mark it as machinery. Read back off * disk it is indistinguishable from something the user typed — and it is the * most repeated text in a real corpus, because running `/pr` thirty times * writes the same two thousand characters thirty times. Mining it produces * directives the user never stated, at counts that look exactly like organic * repetition. * * Detection is retroactive on purpose. A provenance flag written at turn time * would be exact, but it would only help sessions recorded after it shipped, * leaving the existing corpus contaminated for months. Matching against the * command bodies still on disk fixes the history that already exists. The gap * is a template that has since been deleted; that case wants the flag, and is * the reason to add one later. */ export declare function replayFingerprints(templates: Array<{ content: string; }>): string[]; /** True when a user turn is the body of a slash command rather than something typed. */ export declare function isReplayedTurn(text: string, fingerprints: string[]): boolean; /** * Render a session as plain text for the model. * * User turns the user actually typed go in whole and unfiltered — any * truncation there would quietly reintroduce the recall problem the old regex * gate had. What does not go in is text the user's tooling replayed: its own * past digests, and slash-command bodies. * * Assistant prose is dropped: it is the bulk of a transcript and almost none of * it is evidence about what the *user* wants. Tool calls are kept, because a * failure-then-pass is a fix. Successful tool output is dropped: it is a file * or a command's stdout, not a statement by anyone, and feeding it to a miner * looking for directives yields lines lifted out of plan files and configs * attributed to the user. */ export declare function renderTranscript(session: MinableSession, fingerprints?: string[]): string; /** Everything the user actually said in a session, normalized, for checking quotes against. */ export declare function spokenText(session: MinableSession, fingerprints?: string[]): string; /** * Drop candidates whose quote cannot be found in what the user said. * * The miner is told to quote verbatim and the digest renders every quote inside * quotation marks, but on a real corpus a third of them appear nowhere in the * session: paraphrases, merged sentences, and lines lifted out of tool output. * A quote that cannot be located is evidence that cannot be shown, and a * proposal the reader cannot check is worse than one that was never made. * * Whitespace is normalized before comparing, because a directive written in a * markdown file arrives wrapped across lines and the model unwraps it. */ export declare function verifyCandidates(candidates: MinedCandidate[], spoken: string): MinedCandidate[]; /** * Split rendered text on line boundaries, so a chunk never cuts a user turn in * half. A single turn longer than the budget gets its own oversized chunk * rather than being split — losing the second half of a long directive is * exactly the failure this rewrite exists to remove. */ export declare function chunkTranscript(text: string, chunkChars?: number): string[]; /** * Pull the JSON object out of a model response. * * Models fence JSON even when told not to, and occasionally prepend a sentence. * Scanning for the outermost braces is more forgiving than trusting the format * and cheaper than a repair pass — and a chunk whose response cannot be parsed * is skipped, never fatal, because one bad chunk should not lose a whole run. */ export declare function parseCandidates(response: string): MinedCandidate[]; export interface MinerDeps { model: Model; apiKey?: string; headers?: Record; /** * Literal runs from the slash-command bodies in force, from * `replayFingerprints`. User turns matching one are machinery replaying * itself, not the user speaking. */ replayFingerprints?: string[]; } /** * Build the real miner: one model call per chunk of one session. * * Chunks are mined sequentially rather than in parallel. A cold-cache run is * already the expensive path, and firing every chunk of every session at once * is how you trip a provider rate limit on exactly the run that has the most to * do. */ export declare function createLlmMiner(deps: MinerDeps): Miner; //# sourceMappingURL=mine.d.ts.map