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/README.de.md +164 -0
- package/README.es.md +66 -0
- package/README.fr.md +79 -0
- package/README.it.md +66 -0
- package/README.ja.md +65 -0
- package/README.md +53 -8
- package/README.pt-BR.md +66 -0
- package/README.ru.md +66 -0
- package/README.zh-CN.md +66 -0
- package/dist/bench-cli.js +1 -1
- package/dist/bench.d.ts +30 -4
- package/dist/bench.js +1 -1
- package/dist/eval-dataset.d.ts +4 -0
- package/dist/ghost-editor.d.ts +3 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -1
- package/dist/intent.d.ts +58 -3
- package/dist/metrics.d.ts +24 -7
- package/dist/profile-store.d.ts +8 -2
- package/dist/profile.d.ts +9 -1
- package/dist/providers/openai-compat-provider.d.ts +2 -2
- package/dist/providers/router.d.ts +41 -16
- package/dist/providers/types.d.ts +18 -1
- package/dist/replay-cli.js +1 -1
- package/package.json +5 -5
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
|
|
62
|
-
*
|
|
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`
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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;
|
package/dist/profile-store.d.ts
CHANGED
|
@@ -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
|
|
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** (
|
|
13
|
-
* A conversational model completes the developer's
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* the
|
|
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
|
|
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
|