pi-ghost-autocomplete 0.6.0 → 0.7.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/dist/intent.d.ts CHANGED
@@ -39,6 +39,31 @@
39
39
  * → LLM only (skip deterministic)
40
40
  */
41
41
  export type PromptIntent = "command" | "question" | "instruction" | "path-ref" | "prose";
42
+ /**
43
+ * Coarse communication register — *what kind of text* the user is composing,
44
+ * independent of the syntactic {@link PromptIntent}. Drives provider routing
45
+ * (FIM vs chat) and prompt persona (code-edit vs agent-message vs general
46
+ * prose) so general prose no longer gets the "message to a coding agent"
47
+ * framing that distorts non-English / non-instructional text.
48
+ *
49
+ * - `code` — shell commands, file paths, code-shaped drafts.
50
+ * Routed to FIM; deterministic path completion active.
51
+ * - `agent-message` — a request/instruction/question addressed to the AI
52
+ * assistant. Routed to chat with the "continue the
53
+ * developer's message" persona.
54
+ * - `general-prose` — natural-language writing that is NOT an instruction to
55
+ * an agent (notes, documentation, free text). Routed to
56
+ * chat but with a neutral "continue the text" persona
57
+ * that preserves language/voice without reframing it as
58
+ * a task.
59
+ *
60
+ * Separated from `PromptIntent` so the two decisions (syntactic form vs
61
+ * communication target) can evolve independently. E.g. a German imperative
62
+ * ("Bitte ändere …") and English free prose ("I was thinking …") both land
63
+ * on the chat leg, but the former is an agent-message while the latter is
64
+ * general-prose — producing different system prompts.
65
+ */
66
+ export type CompletionRegister = "code" | "agent-message" | "general-prose";
42
67
  /**
43
68
  * Action verbs that strongly signal the user is about to reference a file
44
69
  * path. Used by `isPathContext`, `draftLooksLikePathContext`, and the
@@ -58,8 +83,12 @@ export declare const ACTION_VERBS_ALT: string;
58
83
  /**
59
84
  * Heuristic: does the text before the cursor look like natural-language prose
60
85
  * rather than a command / path context? Returns true when there are 4+ word
61
- * tokens (\w{2,}) — a strong signal the user is writing a sentence, not a
62
- * filename.
86
+ * tokens — a strong signal the user is writing a sentence, not a filename.
87
+ *
88
+ * Uses Unicode-aware tokenization (session 52) so accented characters
89
+ * (German umlauts, French diacritics) count as part of the word instead of
90
+ * fragmenting it. The pre-52 `[A-Za-z]{2,}` regex turned "möchte" into the
91
+ * spurious token "chte", miscounting words and misclassifying prose.
63
92
  *
64
93
  * This prevents the fuzzy matcher from proposing `config.ts` when the user
65
94
  * types "please add a con|" ("con" fuzzy-matches config.ts at score ~75).
@@ -123,6 +152,29 @@ export declare function isPathContext(lineBeforeCursor: string, _token: {
123
152
  * providers use these so the wording stays consistent.
124
153
  */
125
154
  export declare const INTENT_DESCRIPTION: Record<PromptIntent, string>;
155
+ /**
156
+ * Persona/framing instruction per {@link CompletionRegister}. Injected into
157
+ * the chat system prompt so the model knows *what* it is continuing:
158
+ *
159
+ * - `code` — framed as code/command completion (FIM uses its own
160
+ * intent header, so this is mainly for chat providers
161
+ * that receive a code-shaped request).
162
+ * - `agent-message` — "continue the user's message to an AI assistant,
163
+ * don't answer it."
164
+ * - `general-prose` — "continue the text naturally in the same language
165
+ * and voice" — NO coding-agent framing, so general prose isn't distorted
166
+ * into a task instruction.
167
+ */
168
+ export declare const REGISTER_DESCRIPTION: Record<CompletionRegister, string>;
169
+ /**
170
+ * Derive the {@link CompletionRegister} from a classified intent. Pure —
171
+ * used by the editor, router, and providers so all three agree on the
172
+ * register without recomputing it.
173
+ *
174
+ * Unclassified (`undefined`) intent defaults to `code` to preserve the
175
+ * pre-52 default for the common cursor-at-end-of-code case.
176
+ */
177
+ export declare function registerFromIntent(intent: PromptIntent | undefined): CompletionRegister;
126
178
  /**
127
179
  * Classify the user's current prompt intent. Uses simple regex / keyword
128
180
  * heuristics to decide which completion pipeline to activate.
@@ -148,9 +200,12 @@ export declare function classifyPromptIntent(lineBeforeCursor: string, previousL
148
200
  export type ProseReason = ProseContextReason | "single-line";
149
201
  /** Detailed intent classification including the prose-rule reason when
150
202
  * applicable. Lets the editor log *why* the deterministic path was
151
- * suppressed without re-running the heuristic. */
203
+ * suppressed without re-running the heuristic. Includes the derived
204
+ * {@link CompletionRegister} so callers (router, providers) don't need to
205
+ * recompute it. */
152
206
  export interface IntentClassification {
153
207
  intent: PromptIntent;
208
+ register: CompletionRegister;
154
209
  proseReason?: ProseReason;
155
210
  }
156
211
  /**
package/dist/metrics.d.ts CHANGED
@@ -19,18 +19,32 @@ export type RejectionReason = "stale-text" | "newline" | "too-long" | "assistant
19
19
  * whose output the sanitizer dropped, or a request that never reached the
20
20
  * backend — that distinction previously needed synthetic probing.
21
21
  *
22
- * - `model-empty` — the model returned empty/whitespace-only text (the
23
- * Mercury Edit "nothing to insert" case from session 51).
24
- * - `sanitizer-null`— the model returned content but `sanitizeCompletion`
25
- * rejected it (echo / filename-in-prose / markdown).
26
- * - `http-error` — the request failed: missing key, non-2xx, malformed
27
- * JSON, fetch throw, or timeout.
22
+ * - `model-empty` — the model returned empty/whitespace-only text
23
+ * (the Mercury Edit "nothing to insert" case
24
+ * from session 51).
25
+ * - `token-budget-exhausted` — the provider spent its entire `max_tokens`
26
+ * budget before emitting any visible content.
27
+ * Observed with Mercury 2 chat, which consumes
28
+ * budget on internal reasoning tokens first:
29
+ * `finish_reason="length"` with `content=null`
30
+ * and `completion_tokens=0`. A capacity /
31
+ * configuration issue, not a model decline.
32
+ * - `sanitizer-null` — the model returned content but
33
+ * `sanitizeCompletion` rejected it (echo /
34
+ * filename-in-prose / markdown).
35
+ * - `response-schema-error` — the response parsed as JSON but lacked the
36
+ * expected `choices[].message` shape (empty
37
+ * `choices`, missing `message`). Indicates a
38
+ * backend/schema mismatch rather than an empty
39
+ * model verdict.
40
+ * - `http-error` — the request failed: missing key, non-2xx,
41
+ * malformed JSON, fetch throw, or timeout.
28
42
  *
29
43
  * Only set by providers that implement `consumeLastEmptyReason()`. Records from
30
44
  * providers that don't (or that predate this field) omit it — bench buckets
31
45
  * those as `unattributed`.
32
46
  */
33
- export type EmptyReason = "model-empty" | "sanitizer-null" | "http-error";
47
+ export type EmptyReason = "model-empty" | "token-budget-exhausted" | "sanitizer-null" | "response-schema-error" | "http-error";
34
48
  export interface ProviderLatency {
35
49
  provider: string;
36
50
  latencyMs: number;
@@ -45,6 +59,9 @@ export interface MetricsRecord {
45
59
  winner?: string;
46
60
  result: RequestResult;
47
61
  rejectionReason?: RejectionReason;
62
+ /** Session 52 — completion register for this request. Lets bench split
63
+ * abort / latency / quality by register (code vs agent-message vs prose). */
64
+ register?: string;
48
65
  /** When `result === "empty"`, the diagnosed cause (session-51 todo #4). */
49
66
  emptyReason?: EmptyReason;
50
67
  completionLength?: number;
@@ -47,7 +47,7 @@ export declare class ProfileStore {
47
47
  * Capture an accept (full or partial). `weight` defaults to 3 to match
48
48
  * PRD §M2 trie weights. Returns true when a record was emitted.
49
49
  */
50
- captureAccept(prefix: string, completion: string, weight?: number, ts?: number): boolean;
50
+ captureAccept(prefix: string, completion: string, weight?: number, ts?: number, register?: string): boolean;
51
51
  /** Public lookup surfaces — thin wrappers around `profile.ts`. */
52
52
  bestSlashMatch(prefix: string): {
53
53
  cmd: string;
@@ -60,8 +60,14 @@ export declare class ProfileStore {
60
60
  * record. Returns the completion tokens + summed weight, or null when
61
61
  * the prefix has never been the exact key of a captured accept.
62
62
  * `prefix` is hashed in-process; the raw string never leaves memory.
63
+ *
64
+ * Session 52: when `register` is provided, cross-register matches are
65
+ * filtered. A record with a DIFFERENT register is skipped (a code-context
66
+ * accept shouldn't be replayed in a prose context). Records without a
67
+ * register (pre-session-52) are served with a reduced weight (0.7×) so
68
+ * they don't crowd out register-matched candidates but aren't dropped.
63
69
  */
64
- lookupAcceptByPrefix(prefix: string): {
70
+ lookupAcceptByPrefix(prefix: string, register?: string): {
65
71
  completionTokens: string[];
66
72
  weight: number;
67
73
  } | null;
package/dist/profile.d.ts CHANGED
@@ -30,6 +30,13 @@ export interface ProfileRecord {
30
30
  prefixTailTokens?: string[];
31
31
  /** `kind=accept`: tokens of the completion (cap 8). */
32
32
  completionTokens?: string[];
33
+ /** Session 52 — completion register at capture time (`code`,
34
+ * `agent-message`, `general-prose`). When set, `lookupAcceptByPrefix`
35
+ * can skip cross-register replays (a code-context accept shouldn't be
36
+ * served as a provisional ghost in a prose context). Absent on records
37
+ * predating session 52 — loaded as `unknown` and penalized but not
38
+ * dropped. */
39
+ register?: string;
33
40
  }
34
41
  /**
35
42
  * In-memory aggregated view of the profile. The maps are weight-sums per
@@ -43,6 +50,7 @@ export interface ProfileIndex {
43
50
  acceptByPrefix: Map<string, {
44
51
  completionTokens: string[];
45
52
  weight: number;
53
+ register?: string;
46
54
  }>;
47
55
  totalAcceptRecords: number;
48
56
  }
@@ -87,7 +95,7 @@ export declare function recordsFromCommit(text: string, ts?: number): ProfileRec
87
95
  * hashed; `completion` is tokenized. The raw prefix never appears in the
88
96
  * returned record. Drops the whole record if either side fails sanitize.
89
97
  */
90
- export declare function recordFromAccept(prefix: string, completion: string, weight?: number, ts?: number): ProfileRecord | null;
98
+ export declare function recordFromAccept(prefix: string, completion: string, weight?: number, ts?: number, register?: string): ProfileRecord | null;
91
99
  /**
92
100
  * Append one or more records to the JSONL file. Returns silently on write
93
101
  * failure — profile logging must never break ghost completion.
@@ -5,7 +5,7 @@
5
5
  import type { OpenAICompatProviderConfig } from "../config.js";
6
6
  import { type CandidateSet, type GhostProvider, type GhostRequest } from "./types.js";
7
7
  import type { EmptyReason } from "../metrics.js";
8
- import { type PromptIntent } from "../intent.js";
8
+ import { type CompletionRegister, type PromptIntent } from "../intent.js";
9
9
  export interface PreflightResult {
10
10
  ok: boolean;
11
11
  status?: number;
@@ -57,7 +57,7 @@ export declare class OpenAICompatProvider implements GhostProvider {
57
57
  * `PI_GHOST_FIM_CONTEXT_ALL_INTENTS=1` to force-include context for every
58
58
  * intent (A/B against the empty-rate fix).
59
59
  */
60
- export declare function fimContextIncluded(intent: PromptIntent | undefined, allIntentsRaw: string | undefined): boolean;
60
+ export declare function fimContextIncluded(intent: PromptIntent | undefined, allIntentsRaw: string | undefined, register?: CompletionRegister): boolean;
61
61
  /**
62
62
  * Build the FIM prompt/suffix pair from the ghost request.
63
63
  *
@@ -4,27 +4,37 @@
4
4
  */
5
5
  /**
6
6
  * Register-routing provider (PRD §M3h). Holds two legs and dispatches each
7
- * request to the one whose *register* fits the user's intent:
7
+ * request to the one whose {@link CompletionRegister} fits the user's intent:
8
8
  *
9
- * - **FIM leg** (code register) — `command`, `path-ref`, and unclassified
9
+ * - **FIM leg** (`code` register) — `command`, `path-ref`, and unclassified
10
10
  * requests. A code fill-in-the-middle model (Mercury Edit) completes
11
11
  * commands, paths, and code-shaped drafts.
12
- * - **chat leg** (instruction register) — `instruction`, `question`, `prose`.
13
- * A conversational model completes the developer's *message to the AI coding
14
- * agent* (their request/instruction), instead of writing the solution. This
15
- * is the fix for "the autocomplete keeps suggesting solutions": a code-FIM
16
- * model, fed a natural-language prompt, completes it like code.
12
+ * - **chat leg** (`agent-message` + `general-prose` registers) — `instruction`,
13
+ * `question`, `prose`. A conversational model completes the developer's
14
+ * *message to the AI assistant* (for `agent-message`) or continues
15
+ * ordinary prose naturally (for `general-prose`), instead of writing code.
16
+ * This is the fix for "the autocomplete keeps suggesting solutions": a
17
+ * code-FIM model, fed a natural-language prompt, completes it like code.
18
+ *
19
+ * Session 52: routing is now {@link CompletionRegister}-aware via
20
+ * {@link routeToChatByRegister}, falling back to intent-based routing
21
+ * ({@link routeToChat}) when `req.register` is absent (legacy providers).
17
22
  *
18
23
  * When `chat` is null the router degrades to FIM-only — byte-for-byte the
19
24
  * default behavior before routing existed, so enabling router mode without a
20
25
  * reachable chat backend never regresses.
21
26
  */
22
- import type { PromptIntent } from "../intent.js";
27
+ import type { CompletionRegister, PromptIntent } from "../intent.js";
23
28
  import type { EmptyReason } from "../metrics.js";
24
29
  import type { PreflightResult } from "./openai-compat-provider.js";
25
30
  import { type CandidateSet, type GhostProvider, type GhostRequest } from "./types.js";
26
- /** Pure routing predicate — exported for direct testing. */
31
+ /** Pure routing predicate — exported for direct testing. Legacy: routes
32
+ * based on intent. Prefer {@link routeToChatByRegister} when the register
33
+ * is available. */
27
34
  export declare function routeToChat(intent: PromptIntent | undefined): boolean;
35
+ /** Pure routing predicate based on {@link CompletionRegister} (session 52).
36
+ * When `register` is absent, falls back to deriving it from `intent`. */
37
+ export declare function routeToChatByRegister(register: CompletionRegister | undefined, intent: PromptIntent | undefined): boolean;
28
38
  export declare class RouterProvider implements GhostProvider {
29
39
  private readonly fim;
30
40
  private readonly chat;
@@ -35,15 +45,30 @@ export declare class RouterProvider implements GhostProvider {
35
45
  constructor(fim: GhostProvider, chat: GhostProvider | null);
36
46
  get debounceMs(): number;
37
47
  /**
38
- * Which leg an intent resolves to — `"chat"` only when a chat leg is
39
- * present AND the intent is natural-language; otherwise `"fim"` (including
40
- * the chat-absent fallback). Pure; used by the editor to tag metrics with
41
- * the served register so bench can split FIM-vs-chat quality.
48
+ * Register-aware debounce (session 52). Natural-language registers get a
49
+ * longer debounce because the user types more deliberately and the model
50
+ * takes longer to respond — a short debounce wastes API calls that get
51
+ * aborted by the next keystroke. Code-shaped requests keep the base
52
+ * debounce since they're short and benefit from speed.
53
+ *
54
+ * Multipliers are overridable via env vars for A/B tuning:
55
+ * `PI_GHOST_DEBOUNCE_AGENT_MULT` (default 1.5),
56
+ * `PI_GHOST_DEBOUNCE_PROSE_MULT` (default 1.8).
57
+ */
58
+ debounceMsFor(register?: CompletionRegister): number;
59
+ /**
60
+ * Which leg a request resolves to — `"chat"` only when a chat leg is
61
+ * present AND the register/intent is natural-language; otherwise `"fim"`
62
+ * (including the chat-absent fallback). Pure; used by the editor to tag
63
+ * metrics with the served register so bench can split FIM-vs-chat quality.
64
+ *
65
+ * Session 52: prefers `register` over `intent` so the coarser register
66
+ * drives routing. Falls back to intent when register is absent.
42
67
  */
43
- legFor(intent: PromptIntent | undefined): "fim" | "chat";
68
+ legFor(intent: PromptIntent | undefined, register?: CompletionRegister): "fim" | "chat";
44
69
  /** Choose the leg for this request. Chat leg only when present AND the
45
- * intent is natural-language; otherwise FIM. Records the choice so
46
- * `consumeLastEmptyReason` can forward to the leg that actually ran. */
70
+ * intent/register is natural-language; otherwise FIM. Records the choice
71
+ * so `consumeLastEmptyReason` can forward to the leg that actually ran. */
47
72
  private leg;
48
73
  /** Forward the empty-cause diagnostic to whichever leg last served a
49
74
  * request. Null when no request has run yet or the leg doesn't track it. */
@@ -3,7 +3,7 @@
3
3
  * SPDX-FileCopyrightText: 2025 Alessandro Pungitore <alessandro@pungitore.ch>
4
4
  */
5
5
  import type { PreflightResult } from "./openai-compat-provider.js";
6
- import type { PromptIntent } from "../intent.js";
6
+ import type { PromptIntent, CompletionRegister } from "../intent.js";
7
7
  import type { EmptyReason } from "../metrics.js";
8
8
  export interface GhostRequest {
9
9
  editorText: string;
@@ -29,6 +29,14 @@ export interface GhostRequest {
29
29
  * classification was added).
30
30
  */
31
31
  intent?: PromptIntent;
32
+ /**
33
+ * Communication register (session 52) — derived from {@link intent} but
34
+ * coarser: `code` / `agent-message` / `general-prose`. The router uses it
35
+ * to choose the FIM vs chat leg; chat providers use it to pick the system
36
+ * prompt persona (agent-message vs general-prose). When absent, providers
37
+ * fall back to deriving it from {@link intent}.
38
+ */
39
+ register?: CompletionRegister;
32
40
  }
33
41
  /**
34
42
  * One candidate completion. `avgLogprob` is the average per-token logprob if
@@ -62,6 +70,15 @@ export interface StreamDelta {
62
70
  export interface GhostProvider {
63
71
  readonly name: string;
64
72
  readonly debounceMs: number;
73
+ /**
74
+ * Optional register-aware debounce (session 52). When present, the editor
75
+ * uses this instead of {@link debounceMs} so natural-language registers
76
+ * (agent-message, general-prose) get a longer debounce than code. This
77
+ * reduces wasted API calls during fast typing without hurting the UX of
78
+ * deliberate prose composition. When absent, callers fall back to
79
+ * {@link debounceMs}.
80
+ */
81
+ debounceMsFor?(register?: CompletionRegister): number;
65
82
  predict(req: GhostRequest): Promise<string | null>;
66
83
  /**
67
84
  * Optional multi-candidate variant. When absent the editor wraps