pi-ghost-autocomplete 0.3.1 → 0.4.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.
Files changed (83) hide show
  1. package/LICENSE +561 -0
  2. package/README.md +464 -236
  3. package/dist/bench-cli.js +1 -47
  4. package/dist/bench.d.ts +137 -5
  5. package/dist/bench.js +1 -241
  6. package/dist/config.d.ts +47 -1
  7. package/dist/context-aggregator.d.ts +18 -6
  8. package/dist/cycle-manager.d.ts +83 -0
  9. package/dist/entropy-stop.d.ts +105 -0
  10. package/dist/eval-dataset.d.ts +55 -0
  11. package/dist/fuzzy-matcher.d.ts +12 -1
  12. package/dist/ghost-editor.d.ts +332 -6
  13. package/dist/index.d.ts +21 -1
  14. package/dist/index.js +1 -210
  15. package/dist/intent.d.ts +179 -0
  16. package/dist/metrics.d.ts +103 -3
  17. package/dist/path-index.d.ts +4 -1
  18. package/dist/path-matcher.d.ts +13 -1
  19. package/dist/profile-store.d.ts +85 -0
  20. package/dist/profile.d.ts +167 -0
  21. package/dist/providers/cloud-provider.d.ts +14 -2
  22. package/dist/providers/local-provider.d.ts +12 -2
  23. package/dist/providers/openai-compat-provider.d.ts +35 -2
  24. package/dist/providers/race.d.ts +62 -2
  25. package/dist/providers/types.d.ts +109 -2
  26. package/dist/recent-paths.d.ts +4 -1
  27. package/dist/replay-cli.js +2 -0
  28. package/dist/replay.d.ts +102 -0
  29. package/package.json +90 -79
  30. package/dist/bench-cli.d.ts +0 -3
  31. package/dist/bench-cli.d.ts.map +0 -1
  32. package/dist/bench-cli.js.map +0 -1
  33. package/dist/bench.d.ts.map +0 -1
  34. package/dist/bench.js.map +0 -1
  35. package/dist/config.d.ts.map +0 -1
  36. package/dist/config.js +0 -187
  37. package/dist/config.js.map +0 -1
  38. package/dist/context-aggregator.d.ts.map +0 -1
  39. package/dist/context-aggregator.js +0 -58
  40. package/dist/context-aggregator.js.map +0 -1
  41. package/dist/fuzzy-matcher.d.ts.map +0 -1
  42. package/dist/fuzzy-matcher.js +0 -160
  43. package/dist/fuzzy-matcher.js.map +0 -1
  44. package/dist/ghost-editor.d.ts.map +0 -1
  45. package/dist/ghost-editor.js +0 -445
  46. package/dist/ghost-editor.js.map +0 -1
  47. package/dist/index.d.ts.map +0 -1
  48. package/dist/index.js.map +0 -1
  49. package/dist/metrics.d.ts.map +0 -1
  50. package/dist/metrics.js +0 -98
  51. package/dist/metrics.js.map +0 -1
  52. package/dist/path-index.d.ts.map +0 -1
  53. package/dist/path-index.js +0 -148
  54. package/dist/path-index.js.map +0 -1
  55. package/dist/path-matcher.d.ts.map +0 -1
  56. package/dist/path-matcher.js +0 -85
  57. package/dist/path-matcher.js.map +0 -1
  58. package/dist/providers/cloud-provider.d.ts.map +0 -1
  59. package/dist/providers/cloud-provider.js +0 -52
  60. package/dist/providers/cloud-provider.js.map +0 -1
  61. package/dist/providers/local-provider.d.ts.map +0 -1
  62. package/dist/providers/local-provider.js +0 -36
  63. package/dist/providers/local-provider.js.map +0 -1
  64. package/dist/providers/openai-compat-provider.d.ts.map +0 -1
  65. package/dist/providers/openai-compat-provider.js +0 -128
  66. package/dist/providers/openai-compat-provider.js.map +0 -1
  67. package/dist/providers/openai-completions-runner.d.ts +0 -28
  68. package/dist/providers/openai-completions-runner.d.ts.map +0 -1
  69. package/dist/providers/openai-completions-runner.js +0 -46
  70. package/dist/providers/openai-completions-runner.js.map +0 -1
  71. package/dist/providers/race.d.ts.map +0 -1
  72. package/dist/providers/race.js +0 -128
  73. package/dist/providers/race.js.map +0 -1
  74. package/dist/providers/truncate.d.ts +0 -7
  75. package/dist/providers/truncate.d.ts.map +0 -1
  76. package/dist/providers/truncate.js +0 -39
  77. package/dist/providers/truncate.js.map +0 -1
  78. package/dist/providers/types.d.ts.map +0 -1
  79. package/dist/providers/types.js +0 -2
  80. package/dist/providers/types.js.map +0 -1
  81. package/dist/recent-paths.d.ts.map +0 -1
  82. package/dist/recent-paths.js +0 -114
  83. package/dist/recent-paths.js.map +0 -1
@@ -1,5 +1,10 @@
1
+ /**
2
+ * SPDX-License-Identifier: SSPL-1.0
3
+ * SPDX-FileCopyrightText: 2025 Alessandro Pungitore <alessandro@pungitore.ch>
4
+ */
1
5
  import type { OpenAICompatProviderConfig } from "../config.js";
2
- import type { GhostProvider, GhostRequest } from "./types.js";
6
+ import type { CandidateSet, GhostProvider, GhostRequest } from "./types.js";
7
+ import { type PromptIntent } from "../intent.js";
3
8
  export interface PreflightResult {
4
9
  ok: boolean;
5
10
  status?: number;
@@ -25,5 +30,33 @@ export declare class OpenAICompatProvider implements GhostProvider {
25
30
  */
26
31
  preflight(): Promise<PreflightResult>;
27
32
  predict(req: GhostRequest): Promise<string | null>;
33
+ /**
34
+ * Multi-candidate prediction. Fires `config.parallelN` FIM requests in
35
+ * parallel and dedupes by exact text. PRD §M3b: FIM does not support
36
+ * the `n` parameter, so parallel POSTs are the only n>1 path. Parallel
37
+ * fire is only useful at temperature > 0 — at temp=0 every call returns
38
+ * the same greedy result. We downgrade silently to n=1 in that case.
39
+ */
40
+ predictMulti(req: GhostRequest): Promise<CandidateSet | null>;
41
+ private effectiveParallelN;
42
+ private fireOne;
28
43
  }
29
- //# sourceMappingURL=openai-compat-provider.d.ts.map
44
+ /**
45
+ * Whether the conversation context should be included in the FIM prompt for a
46
+ * given intent. Pure + exported for direct testing. Set
47
+ * `PI_GHOST_FIM_CONTEXT_ALL_INTENTS=1` to force-include context for every
48
+ * intent (A/B against the empty-rate fix).
49
+ */
50
+ export declare function fimContextIncluded(intent: PromptIntent | undefined, allIntentsRaw: string | undefined): boolean;
51
+ /**
52
+ * Build the FIM prompt/suffix pair from the ghost request.
53
+ *
54
+ * Recent conversation messages are prepended as comments in the prompt so the
55
+ * model has context about what the user is working on — except for `prose` /
56
+ * `question` intents, where the context is dropped to avoid the empty-rate
57
+ * spike described on {@link FIM_CONTEXT_SUPPRESSED_INTENTS}.
58
+ */
59
+ export declare function buildFIMPrompt(req: GhostRequest, maxRecent: number): {
60
+ prompt: string;
61
+ suffix: string;
62
+ };
@@ -1,5 +1,9 @@
1
+ /**
2
+ * SPDX-License-Identifier: SSPL-1.0
3
+ * SPDX-FileCopyrightText: 2025 Alessandro Pungitore <alessandro@pungitore.ch>
4
+ */
1
5
  import type { ProviderLatency } from "../metrics.js";
2
- import type { GhostProvider, GhostRequest } from "./types.js";
6
+ import { type GhostProvider, type GhostRequest, type StreamDelta } from "./types.js";
3
7
  export interface RaceConfig {
4
8
  /** Pause/debounce before race starts. Reported as `debounceMs`. */
5
9
  pauseMs: number;
@@ -11,6 +15,8 @@ export interface RaceConfig {
11
15
  export interface RaceTrace {
12
16
  providers: ProviderLatency[];
13
17
  winner?: string;
18
+ /** Members that the weighting policy declined to fire (PRD §M8). */
19
+ skipped?: string[];
14
20
  }
15
21
  /**
16
22
  * Implements {@link GhostProvider} by racing several providers in parallel.
@@ -26,10 +32,65 @@ export declare class RaceProvider implements GhostProvider {
26
32
  readonly name: string;
27
33
  private cooldownUntil;
28
34
  private lastTrace;
35
+ private readonly stats;
29
36
  constructor(members: GhostProvider[], cfg: RaceConfig);
37
+ /** Snapshot of per-member weighting stats. Test/diagnostic only. */
38
+ getMemberStats(): Array<{
39
+ name: string;
40
+ slowCount: number;
41
+ wins: number;
42
+ skipRemaining: number;
43
+ }>;
30
44
  get debounceMs(): number;
31
45
  consumeLastTrace(): RaceTrace | null;
46
+ /**
47
+ * Cooldown snapshot for speculative cache warm. The editor consults this
48
+ * synchronously in `handleTextChange` to decide whether the backspace-of-N
49
+ * branch may serve a result (it is the most aggressive branch and is only
50
+ * appropriate when a fresh race would no-op anyway). See PRD §6 M1b.
51
+ */
52
+ getCooldownState(now?: number): {
53
+ active: boolean;
54
+ remainingMs: number;
55
+ };
32
56
  predict(req: GhostRequest): Promise<string | null>;
57
+ /**
58
+ * After a race resolves, update per-member rolling stats and decide who
59
+ * gets a skip penalty for the next `SKIP_RACES`. A member earns "slow"
60
+ * if it ran but didn't win this race (when there was a winner).
61
+ *
62
+ * `userAborted` distinguishes user-keystroke cancellation from
63
+ * race-cancellation-by-winner. Member's trace result is "aborted" in
64
+ * both cases; we only penalize the latter (the member could have run
65
+ * to completion but lost the race). User keystrokes shouldn't drift
66
+ * the weighting at all.
67
+ */
68
+ private recordRaceOutcome;
69
+ /**
70
+ * PRD §M3.5a — streaming variant of `predict`. Each member runs its own
71
+ * stream under a dedicated AbortController; first member to emit a
72
+ * non-empty delta wins. Losing members are aborted and their iterators
73
+ * released. The winner's remaining chunks pass through untouched.
74
+ *
75
+ * Trace + stats are recorded identically to `predict()` so M8 weighting
76
+ * works the same way: winner = first to first-chunk; losers that also
77
+ * produced are still marked `produced` (they did emit), losers aborted
78
+ * before producing are `aborted`.
79
+ */
80
+ predictStream(req: GhostRequest): AsyncIterable<StreamDelta>;
81
+ /**
82
+ * Pump a member's stream until its first non-empty delta. Resolves to
83
+ * the parked iterator + that delta when produced; null on
84
+ * empty/aborted/error. Trace is appended for terminal outcomes; the
85
+ * `produced` entry is pushed once the first non-empty delta arrives so
86
+ * its latency reflects TTFT, not end-of-stream.
87
+ */
88
+ private firstChunkOf;
89
+ /**
90
+ * Reset all rolling stats (PRD §M8: "Reset state on `setProvider` /
91
+ * extension reload"). Public for editor reset paths.
92
+ */
93
+ resetStats(): void;
33
94
  private runMember;
34
95
  }
35
96
  /**
@@ -37,4 +98,3 @@ export declare class RaceProvider implements GhostProvider {
37
98
  * all promises produce null (or throw). Never rejects.
38
99
  */
39
100
  export declare function firstNonNull<T>(promises: Array<Promise<T | null>>): Promise<T | null>;
40
- //# sourceMappingURL=race.d.ts.map
@@ -1,18 +1,125 @@
1
+ /**
2
+ * SPDX-License-Identifier: SSPL-1.0
3
+ * SPDX-FileCopyrightText: 2025 Alessandro Pungitore <alessandro@pungitore.ch>
4
+ */
1
5
  import type { PreflightResult } from "./openai-compat-provider.js";
6
+ import type { PromptIntent } from "../intent.js";
2
7
  export interface GhostRequest {
3
8
  editorText: string;
4
9
  editorTextBeforeCursor: string;
10
+ /**
11
+ * Recent conversation messages, oldest-first. `role: "tool"` entries
12
+ * may carry a `toolName` for richer prompt formatting (PRD §M6a). The
13
+ * editor only surfaces tool entries when `includeTools` is enabled in
14
+ * `context-aggregator.getRecentMessages`.
15
+ */
5
16
  recentMessages: Array<{
6
- role: "user" | "assistant";
17
+ role: "user" | "assistant" | "tool";
7
18
  text: string;
19
+ toolName?: string;
8
20
  }>;
9
21
  cwd: string;
10
22
  signal: AbortSignal;
23
+ /**
24
+ * Classified intent of the current prompt line. When set, providers use
25
+ * it to tailor the system prompt / FIM prompt so the model receives
26
+ * explicit signals about what kind of completion to produce.
27
+ * Undefined for backward compatibility (providers built before intent
28
+ * classification was added).
29
+ */
30
+ intent?: PromptIntent;
31
+ }
32
+ /**
33
+ * One candidate completion. `avgLogprob` is the average per-token logprob if
34
+ * the provider returned them; undefined when not supported. Reranker treats
35
+ * missing logprobs as the neutral mid-value (0.5 after normalization) per
36
+ * PRD §M3c.
37
+ */
38
+ export interface GhostCandidate {
39
+ text: string;
40
+ avgLogprob?: number;
41
+ }
42
+ /**
43
+ * Result of a multi-candidate prediction. `candidates` is ordered as the
44
+ * provider returned them (no rerank applied yet). Single-candidate providers
45
+ * return a single-element array.
46
+ */
47
+ export interface CandidateSet {
48
+ candidates: GhostCandidate[];
49
+ }
50
+ /**
51
+ * One streaming delta from a provider. PRD §M3.5a — captures both the
52
+ * incremental piece (`delta`) and the running concatenation (`cumulative`)
53
+ * so consumers don't have to track state. `firstChunk` flags the very
54
+ * first non-empty delta, which is what the editor stamps as `ttftMs`.
55
+ */
56
+ export interface StreamDelta {
57
+ delta: string;
58
+ cumulative: string;
59
+ firstChunk: boolean;
11
60
  }
12
61
  export interface GhostProvider {
13
62
  readonly name: string;
14
63
  readonly debounceMs: number;
15
64
  predict(req: GhostRequest): Promise<string | null>;
65
+ /**
66
+ * Optional multi-candidate variant. When absent the editor wraps
67
+ * `predict()` via `wrapAsMulti`. See PRD §M3a.
68
+ */
69
+ predictMulti?(req: GhostRequest): Promise<CandidateSet | null>;
70
+ /**
71
+ * Optional streaming variant. When present the editor uses it on
72
+ * single-candidate paths to capture `ttftMs` and (in a follow-up
73
+ * iteration) drive incremental UI rendering. Providers that don't
74
+ * stream (e.g. Mercury Edit FIM) omit this method.
75
+ */
76
+ predictStream?(req: GhostRequest): AsyncIterable<StreamDelta>;
16
77
  preflight?(): Promise<boolean | PreflightResult>;
78
+ /**
79
+ * PRD §M3g / §M7 capability flag — true iff the provider populates
80
+ * per-token logprob values on streamed `StreamDelta` events (M7
81
+ * entropy-stop input). `StreamDelta` will be extended in a future
82
+ * change to include per-token logprob data. No provider currently sets
83
+ * this since `@earendil-works/pi-ai` does not surface logprobs; left in
84
+ * place so M7 wiring can flip on once upstream support lands without
85
+ * changing the diagnostics surface. Wording aligns with
86
+ * `GhostCandidate.avgLogprob`.
87
+ */
88
+ readonly supportsLogprobs?: boolean;
17
89
  }
18
- //# sourceMappingURL=types.d.ts.map
90
+ /**
91
+ * Adapt a `predict()`-only provider into the `CandidateSet | null` shape.
92
+ * Used by the editor to give every provider a uniform multi-candidate
93
+ * surface without forcing third-party implementations to upgrade.
94
+ *
95
+ * Wrapped candidates intentionally have `avgLogprob: undefined` — the
96
+ * `predict()` signature returns only text, so logprobs aren't recoverable.
97
+ * The reranker normalizes missing logprobs to the neutral mid-value (0.5)
98
+ * per PRD §M3c, so single-candidate wrappers aren't penalized against
99
+ * providers that do return logprobs.
100
+ */
101
+ export declare function wrapAsMulti(provider: GhostProvider, req: GhostRequest): Promise<CandidateSet | null>;
102
+ /**
103
+ * Adapt any provider to the streaming surface. Providers with
104
+ * `predictStream` pass through unchanged; non-streaming providers get a
105
+ * single-delta stream emitted after `predict()` resolves. `firstChunk`
106
+ * is true on the only emission in the non-streaming case.
107
+ *
108
+ * Yields nothing when the provider returns null/empty. Consumer code
109
+ * pattern:
110
+ *
111
+ * ```ts
112
+ * let full = "";
113
+ * for await (const { cumulative } of wrapAsStream(p, req)) full = cumulative;
114
+ * ```
115
+ */
116
+ export declare function wrapAsStream(provider: GhostProvider, req: GhostRequest): AsyncIterable<StreamDelta>;
117
+ /**
118
+ * Drain a stream into its final cumulative text. Records the timestamp
119
+ * of the first non-empty chunk on `out.ttftMs` (relative to `startedAt`)
120
+ * for the caller. The result is the full assembled text, or null if the
121
+ * stream produced no content.
122
+ */
123
+ export declare function streamToText(stream: AsyncIterable<StreamDelta>, startedAt: number, out?: {
124
+ ttftMs?: number;
125
+ }): Promise<string | null>;
@@ -1,3 +1,7 @@
1
+ /**
2
+ * SPDX-License-Identifier: SSPL-1.0
3
+ * SPDX-FileCopyrightText: 2025 Alessandro Pungitore <alessandro@pungitore.ch>
4
+ */
1
5
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
6
  import { type RecentMessage } from "./context-aggregator.js";
3
7
  /**
@@ -22,4 +26,3 @@ export declare function recentBoost(matchPath: string, recent: Set<string> | nul
22
26
  * assistant messages. Oldest-first.
23
27
  */
24
28
  export declare function getRoleWeightedRecent(ctx: ExtensionContext, userMax: number, assistantMax: number): RecentMessage[];
25
- //# sourceMappingURL=recent-paths.d.ts.map