billion-context-pi-lean 0.1.69-lean.1 → 0.2.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 (60) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +141 -81
  3. package/dist/abort.d.ts +1 -0
  4. package/dist/branch-guard.d.ts +8 -0
  5. package/dist/cache-tool.d.ts +12 -0
  6. package/dist/chunk-PPYOZRUP.js +18 -0
  7. package/dist/chunk-PPYOZRUP.js.map +1 -0
  8. package/dist/commands.d.ts +8 -0
  9. package/dist/compat.d.ts +23 -0
  10. package/dist/compress-model.d.ts +65 -0
  11. package/dist/compress-tool.d.ts +76 -0
  12. package/dist/config-dir.d.ts +32 -0
  13. package/dist/config-panel.d.ts +61 -0
  14. package/dist/config.d.ts +296 -0
  15. package/dist/contract.d.ts +51 -0
  16. package/dist/contract.js +9 -0
  17. package/dist/contract.js.map +1 -0
  18. package/dist/decompress-tool.d.ts +19 -0
  19. package/dist/degeneration.d.ts +98 -0
  20. package/dist/export.d.ts +20 -0
  21. package/dist/floor-stale.d.ts +31 -0
  22. package/dist/host.d.ts +15 -0
  23. package/dist/idle-compact.d.ts +39 -0
  24. package/dist/index.d.ts +10 -0
  25. package/dist/index.js +23169 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/live-only-tail.d.ts +10 -0
  28. package/dist/log.d.ts +18 -0
  29. package/dist/messages.d.ts +85 -0
  30. package/dist/omp.d.ts +19 -0
  31. package/dist/overflow-selfheal.d.ts +132 -0
  32. package/dist/prompt-pack.d.ts +53 -0
  33. package/dist/proxy-detect.d.ts +21 -0
  34. package/dist/reasoning-drop.d.ts +68 -0
  35. package/dist/rule-tool.d.ts +8 -0
  36. package/dist/runtime.d.ts +199 -0
  37. package/dist/search-index.d.ts +20 -0
  38. package/dist/search-tool.d.ts +10 -0
  39. package/dist/sequence-match.d.ts +7 -0
  40. package/dist/session-log.d.ts +23 -0
  41. package/dist/state-rebuild.d.ts +30 -0
  42. package/dist/state.d.ts +44 -0
  43. package/dist/status-tool.d.ts +13 -0
  44. package/dist/strip-images.d.ts +18 -0
  45. package/dist/summary-sanitize.d.ts +8 -0
  46. package/dist/surface.d.ts +17 -0
  47. package/dist/system-passthrough.d.ts +4 -0
  48. package/dist/system-prompt.d.ts +20 -0
  49. package/dist/tag-tokens.d.ts +3 -0
  50. package/dist/throttle-retry.d.ts +52 -0
  51. package/dist/tokens.d.ts +61 -0
  52. package/dist/tool-guardrails.d.ts +33 -0
  53. package/dist/tool-pair-sanitizer.d.ts +8 -0
  54. package/dist/turn-boundary.d.ts +53 -0
  55. package/dist/user-config.d.ts +67 -0
  56. package/lean/index.ts +376 -0
  57. package/package.json +85 -36
  58. package/schema/bcp-block-v1.json +42 -0
  59. package/README.zh-CN.md +0 -79
  60. package/index.ts +0 -266
@@ -0,0 +1,10 @@
1
+ import type { SessionEntry, SessionMessageEntry } from "@earendil-works/pi-coding-agent";
2
+ type AgentMessage = SessionMessageEntry["message"];
3
+ export declare function liveOnlyTail(entries: SessionEntry[], live: AgentMessage[]): AgentMessage[] | null;
4
+ /** Cached variant of liveOnlyTail keyed by session id. Behaviourally identical
5
+ * (same tail or null); only the prefix proof is memoized. Doubt always falls
6
+ * back to the full walk. */
7
+ export declare function liveOnlyTailCached(sid: string, entries: SessionEntry[], live: AgentMessage[]): AgentMessage[] | null;
8
+ /** Drop a session's alignment cache (session_shutdown for memory hygiene). */
9
+ export declare function dropLiveOnlyTailCache(sid: string): void;
10
+ export {};
package/dist/log.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ export declare function setDebugEnabled(enabled: boolean): void;
2
+ export type LogLevel = "error" | "warn" | "info" | "debug";
3
+ export declare function closeLogStream(): void;
4
+ export declare function logError(scope: string, fields: Record<string, unknown>): void;
5
+ export declare function logWarn(scope: string, fields: Record<string, unknown>): void;
6
+ export declare function logInfo(scope: string, fields: Record<string, unknown>): void;
7
+ export declare function logThrow(scope: string, err: unknown, extra?: Record<string, unknown>): void;
8
+ export declare const debug: {
9
+ readonly enabled: boolean;
10
+ readonly logFile: string;
11
+ event(scope: string, fields: Record<string, unknown>): void;
12
+ };
13
+ export declare const logger: {
14
+ error: typeof logError;
15
+ warn: typeof logWarn;
16
+ info: typeof logInfo;
17
+ debug(scope: string, fields: Record<string, unknown>): void;
18
+ };
@@ -0,0 +1,85 @@
1
+ import type { SessionEntry, SessionMessageEntry } from "@earendil-works/pi-coding-agent";
2
+ import { type Config, type CoreMessage } from "acp-kernel";
3
+ import type { TurnBoundaryEntry } from "./turn-boundary.js";
4
+ type AgentMessage = SessionMessageEntry["message"];
5
+ /** Host-injected status-panel custom messages (written by src/commands.ts) —
6
+ * UI-only, never projected into LLM context, so never user-like entries. */
7
+ export declare const ACP_STATUS_CUSTOM_TYPE = "acp-status";
8
+ /** /acp-export handoff docs (src/export.ts) — UI-only transcript output,
9
+ * persistent in the session but never projected into LLM context (#255). */
10
+ export declare const ACP_EXPORT_CUSTOM_TYPE = "acp-export";
11
+ /** /acp-rule outputs (src/commands.ts) — UI-only transcript output, same
12
+ * exclusion as acp-status/acp-export (#526): the rules themselves already
13
+ * reach the model through the feature's own channel, so the human-facing
14
+ * report must never be re-billed as model context. */
15
+ export declare const ACP_RULE_CUSTOM_TYPE = "acp-rule";
16
+ /** Nudge persistence record (issue #326): written via pi.appendEntry as a
17
+ * type:"custom" entry — never projected into the sent view (see above), so
18
+ * the compact one-liner stays out of model context while surviving restarts. */
19
+ export declare const ACP_NUDGE_CUSTOM_TYPE = "acp-nudge";
20
+ export interface AcpNudgeRecord {
21
+ text: string;
22
+ }
23
+ /** True for host-injected custom_message entries that participate in LLM
24
+ * context: non-empty custom_message except the UI-only types in
25
+ * CONTEXT_EXCLUDED_CUSTOM_TYPES (acp-status panels, acp-export docs,
26
+ * acp-rule reports)
27
+ * (Pi-native projection semantics, session-manager.d.ts). The non-empty gate
28
+ * uses the exact same extractText check as the projection below, so an entry
29
+ * either enters context or it doesn't — and only entries that enter context
30
+ * can delimit a turn (#364 acceptance c: empty control signals start none).
31
+ * Shared by the turn-boundary predicate in src/turn-boundary.ts so the two
32
+ * views can never drift apart; defined here (not there) because it needs
33
+ * extractText — importing that back would create a cycle. */
34
+ export declare function isCustomMessageEntry(entry: TurnBoundaryEntry): entry is TurnBoundaryEntry & {
35
+ type: "custom_message";
36
+ };
37
+ export declare function entriesToCoreMessages(entries: SessionEntry[]): CoreMessage[];
38
+ /** Append-only incremental cache for entriesToCoreMessages (issue #561).
39
+ *
40
+ * Projection is a pure prefix-stable function: each entry contributes a fixed
41
+ * slice of CoreMessages, so projecting entries[0..n) then appending the
42
+ * projection of entries[n..m) equals projecting entries[0..m) — as long as the
43
+ * first n entries are UNCHANGED. Hosts rebuild the entries array (and the entry
44
+ * objects) from the append-only session jsonl every turn, so identity caching
45
+ * is useless; validate structurally instead:
46
+ * - count only ever GROWS (a drop = host rewind/truncate → full reproject),
47
+ * - the first and last cached boundary entries still carry the same ids
48
+ * (canaries against in-place rewrites the count check cannot see),
49
+ * - at most MAX_INCREMENTAL_ENTRIES new entries per turn.
50
+ * Any doubt → full entriesToCoreMessages, identical result, just slower. */
51
+ export declare class EntryProjectionCache {
52
+ private count;
53
+ private firstId;
54
+ private lastId;
55
+ private cores;
56
+ /** Project `entries`, reusing the previous turn's work when the array is a
57
+ * structural extension of the last one seen. Returns a defensive copy: the
58
+ * internal array is shared across turns, and callers hand it to kernel code
59
+ * that must never observe (or mutate) cross-turn state. */
60
+ project(entries: SessionEntry[]): CoreMessage[];
61
+ /** Drop the cache (state rebuilt, session switched). */
62
+ reset(): void;
63
+ }
64
+ export declare function extractText(content: unknown): string;
65
+ export declare function thinkingTokenCount(content: unknown): number;
66
+ export declare function countImageBlocks(content: unknown): number;
67
+ export declare function messageIdentity(message: unknown): string;
68
+ export declare function messageRef(message: unknown): string | undefined;
69
+ export declare const TRUNCATION_MARKER = "[truncated for context space]";
70
+ export declare function matchesStoredText(stored: string, visible: string): boolean;
71
+ export declare function coreOutToAgentMessages(coreOut: CoreMessage[], originalById: Map<string, AgentMessage>): AgentMessage[];
72
+ export interface LatestUserGuardResult {
73
+ messages: CoreMessage[];
74
+ /** Id of the latest user message whose kernel truncation was undone. */
75
+ restoredId?: string;
76
+ /** Estimated request size after the guard (tokenCount scale). */
77
+ remainingTokens: number;
78
+ }
79
+ /** Lean: the kernel protects the most recent user message from compression
80
+ * but its emergency truncation only protects the last N messages, so a big
81
+ * prompt followed by a few tool rounds lost its body without any summary.
82
+ * Undo that truncation and give the reclaimed budget to the other candidates
83
+ * (the kernel's own truncator, run with the user message hidden). */
84
+ export declare function protectLatestUserMessage(inMessages: CoreMessage[], outMessages: CoreMessage[], config: Config, tokenCount: number): LatestUserGuardResult;
85
+ export {};
package/dist/omp.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shown (and logged) when the extension detects an unsupported host and stands down.
3
+ *
4
+ * Unsupported = no Pi `buildContextEntries()` API AND no `PI_ACP_FORK_HOST` declaration
5
+ * (see ./host.ts). OMP (oh-my-pi) falls into this by default: its in-process live-entries
6
+ * integration diverges the nudge's example refs from the session's real refs, so compress
7
+ * calls fail with "does not exist in this session" ([#234]). The billion-context proxy runs
8
+ * compression server-side (it owns the ref coordinate space) and works on OMP.
9
+ */
10
+ export declare const UNSUPPORTED_HOST_MESSAGE: string;
11
+ /**
12
+ * Shown (and logged) when a session runs on a host that declared itself a
13
+ * Pi-compatible fork via PI_ACP_FORK_HOST (see ./host.ts). Live-tail refs are
14
+ * content-addressed and stable across context fires (#459), which assumes the
15
+ * host's view of the session grows append-only; a host that rewrites or
16
+ * shrinks in-flight messages can still drift refs, so long sessions on such
17
+ * hosts are safer on the proxy (it owns the ref coordinate space server-side).
18
+ */
19
+ export declare const FORK_HOST_WARNING_MESSAGE: string;
@@ -0,0 +1,132 @@
1
+ export declare const OVERFLOW_MARKER: RegExp;
2
+ export interface OverflowInfo {
3
+ isOverflow: boolean;
4
+ /** The real context window, when the provider stated it in the error. */
5
+ window?: number;
6
+ message: string;
7
+ }
8
+ export declare function inspectOverflowMessage(haystack: string | undefined | null): OverflowInfo;
9
+ export declare const NO_BODY_4XX_MARKER: RegExp;
10
+ export declare function isNoBody4xxError(haystack: string | undefined | null): boolean;
11
+ /** Corroboration threshold for arming on an ambiguous no-body 4xx: the
12
+ * calibrated sent-view estimate must be at least this fraction of the
13
+ * effective limit (same basis as the turn log's `pct`). */
14
+ export declare const NO_BODY_ARM_RATIO = 0.5;
15
+ /** Decision returned by OverflowEpisode.onNoBody4xx(). */
16
+ export interface NoBodyDecision {
17
+ arm: boolean;
18
+ /** Consecutive no-body 4xx errors since the last successful assistant turn. */
19
+ consecutive: number;
20
+ /** Recorded sent-view estimate / effective limit, when both were seen. */
21
+ ratio: number | null;
22
+ }
23
+ /** Default cap on the output-headroom reservation, as a fraction of the
24
+ * context window (issue #207). Reserving the FULL registered max output
25
+ * capability halves the effective input budget on models whose maxTokens is a
26
+ * large share of the window (e.g. 131072 on a 262144 window → the 75% force-
27
+ * compress band fires at ~37% of the full window), while real per-turn
28
+ * replies rarely approach that. Capping at 25% keeps the guarantee where it
29
+ * matters — any single-turn reply up to the reserved amount still fits at the
30
+ * 95% emergency threshold — while bounding the budget loss. A reply longer
31
+ * than the reservation overflows once; the overflow self-heal (learned window
32
+ * + armed emergency) recovers it on the next turn. */
33
+ export declare const DEFAULT_OUTPUT_HEADROOM_MAX_PCT = 0.25;
34
+ /** Resolve the user's `outputHeadroomMaxPct` (ratio or "N%" string) to a
35
+ * numeric cap, falling back to DEFAULT_OUTPUT_HEADROOM_MAX_PCT when unset.
36
+ * Shared by every headroom call site (context transform, /acp, acp_status)
37
+ * so they all measure against the SAME capped limit (issue #207/#267). */
38
+ export declare function resolveOutputHeadroomCap(value: number | string | undefined): number;
39
+ /**
40
+ * Reserve the model's output budget from the context window, so the kernel's
41
+ * nudge/truncate bands sit below (window - reserved) and the context leaves
42
+ * room for the model's reply. This prevents the "context + output > window"
43
+ * overflow on a small window (agents routinely set a large max output).
44
+ *
45
+ * `capPct` bounds the reservation as a fraction of the window: reserved =
46
+ * min(maxOutput, capPct * window). The default (1) preserves the original
47
+ * full-capability reservation; the adapter passes DEFAULT_OUTPUT_HEADROOM_MAX_PCT
48
+ * (or the user's `outputHeadroomMaxPct`) so oversized registered capabilities
49
+ * no longer eat most of the input budget (issue #207). capPct semantics:
50
+ * - 0 → no reservation (window returned unchanged)
51
+ * - (0, 1) → reservation capped at capPct * window
52
+ * - >= 1 → legacy behavior (full maxOutput reserved)
53
+ * - non-finite → legacy behavior (treated as "not provided")
54
+ * Returns the window unchanged when maxOutput is not usable (non-positive,
55
+ * non-finite, or >= window — a maxOutput >= window request is degenerate and is
56
+ * left to the overflow self-heal).
57
+ */
58
+ export declare function reserveOutputHeadroom(window: number, maxOutput: number, capPct?: number): number;
59
+ /**
60
+ * Whether the OUTPUT budget should be reserved from the context window at
61
+ * all. Anthropic's Messages API enforces the input limit INDEPENDENTLY of
62
+ * max_tokens (the output budget is separate — input up to the window works
63
+ * with any max_tokens), so reserving the model's output capability would
64
+ * shift the nudge/truncate bands down by maxTokens on every session with no
65
+ * safety gain (e.g. a 200k model with a 64k output budget would start
66
+ * compressing around 136k). The OpenAI-family APIs count output against the
67
+ * window, so the reservation is only needed there. Unknown APIs reserve
68
+ * (conservative — a missed reservation at worst overflows once and the
69
+ * self-heal corrects it).
70
+ */
71
+ export declare function shouldReserveOutputHeadroom(api: string | undefined): boolean;
72
+ /**
73
+ * Apply the output-headroom reservation to a resolved config's modelContextLimit
74
+ * (see reserveOutputHeadroom / shouldReserveOutputHeadroom). Returns a NEW config
75
+ * (never mutates the input) so the shared resolved config stays untouched. Used
76
+ * by BOTH the live context transform and the read-only panel surfaces (/acp,
77
+ * acp_status) so every percentage is measured against the SAME real request
78
+ * limit — otherwise the panel reports against the full window while the nudge
79
+ * bands run against (window − maxOutput) (issue #267).
80
+ *
81
+ * `capPct` bounds the reservation as a fraction of the window (see
82
+ * reserveOutputHeadroom); callers pass resolveOutputHeadroomCap(
83
+ * adapter.outputHeadroomMaxPct) so the panel and the nudge bands share the
84
+ * same capped limit (issue #207). The default (1) preserves the legacy
85
+ * full-capability reservation for callers that don't pass a cap.
86
+ */
87
+ export declare function applyOutputHeadroom<T extends {
88
+ modelContextLimit: number;
89
+ }>(config: T, model: {
90
+ maxTokens?: number;
91
+ api?: string;
92
+ } | undefined, capPct?: number): T;
93
+ export declare class OverflowEpisode {
94
+ /** Real windows learned from overflow errors, keyed by model id. A learned
95
+ * window is model-specific: switching to a bigger model mid-session must
96
+ * not inherit the smaller model's learned limit (that would re-center the
97
+ * bands below the new model's real window → premature compression). */
98
+ private learned;
99
+ learnedWindowFor(modelId: string): number | null;
100
+ setLearnedWindow(modelId: string, window: number): void;
101
+ /** When true, the next context event forces usage >=95% (emergency). Kept
102
+ * session-scoped (not per-model): the context did not shrink, so the next
103
+ * turn needs the emergency regardless of which model answers it. */
104
+ armed: boolean;
105
+ /** Sent-view estimate + effective limit recorded by the context event —
106
+ * the numbers of the request that just ended. */
107
+ private sentTokens;
108
+ private sentLimit;
109
+ /** Consecutive no-body 4xx errors since the last successful assistant
110
+ * turn. 0 at session start (the episode is created per session). */
111
+ private noBody4xx;
112
+ noteSentView(tokens: number, limit: number): void;
113
+ /** A successful assistant turn unwedged the request loop: the consecutive
114
+ * no-body count restarts from zero. */
115
+ noteSuccess(): void;
116
+ /**
117
+ * Record one ambiguous no-body 4xx and decide whether it corroborates a
118
+ * probable context overflow. Guards against the false positive (the same
119
+ * text serves non-overflow 4xx): arm only when the current sent-view
120
+ * estimate is >= NO_BODY_ARM_RATIO of the effective limit, OR this is the
121
+ * >=2nd consecutive no-body since the last success. The consecutive guard
122
+ * recovers the dead-loop even at a low ratio (the incident ran at ~24%:
123
+ * the estimate under-reported vs the input+max_tokens cap) — a genuine
124
+ * overflow fails identically while the context is unchanged, so repeated
125
+ * no-body errors with no successful turn in between are size-dependent by
126
+ * construction. No window can be parsed from a bodyless error, so when
127
+ * this arms, the emergency uses the already-resolved effective limit (no
128
+ * window is learned).
129
+ */
130
+ onNoBody4xx(): NoBodyDecision;
131
+ reset(): void;
132
+ }
@@ -0,0 +1,53 @@
1
+ import { builtinSource, createDirPackSource, createPackResolver, defaultPack, isValidPackName, leanPack, sanitizePackSurface } from "acp-kernel";
2
+ import type { Pack, PackResolver, PackSource, PackSurface, PromptPackFile, Prompts } from "acp-kernel";
3
+ import type { AdapterConfig } from "./config.js";
4
+ import { type PiPromptSections } from "./system-prompt.js";
5
+ import { type AcpToolName, type NudgeSectionsConfig, type ToolPromptsConfig } from "./surface.js";
6
+ export { builtinSource, createDirPackSource, createPackResolver, defaultPack, isValidPackName, leanPack, sanitizePackSurface };
7
+ export type { Pack, PackResolver, PackSource, PackSurface, PromptPackFile };
8
+ export declare function defaultPackSources(cwd: string): PackSource[];
9
+ export declare function packResolver(cwd: string): PackResolver;
10
+ export declare function discoverPack(name: string, cwd: string): Pack | null;
11
+ export declare function resolvePackName(adapter: AdapterConfig, provider?: string, modelId?: string): string;
12
+ export declare function resolveActivePack(adapter: AdapterConfig, cwd: string, provider?: string, modelId?: string, resolver?: PackResolver): Pack;
13
+ export interface SurfaceMeta {
14
+ pack: string;
15
+ packVersion?: string;
16
+ host: string;
17
+ }
18
+ /** Surface meta from an already-resolved pack (avoids double resolution
19
+ * when the caller already holds the active Pack). */
20
+ export declare function surfaceMetaOf(pack: Pack, requested: string): SurfaceMeta;
21
+ /** Host-declared surface facts for the kernel's status reports (acp-kernel
22
+ * StatusReportMeta). Which pack is active is host policy — the kernel only
23
+ * renders what the host declares about the active surface. */
24
+ export declare function resolveSurfaceMeta(adapter: AdapterConfig, cwd: string, provider?: string, modelId?: string, resolver?: PackResolver): SurfaceMeta;
25
+ export interface PiToolExtras {
26
+ promptSnippet?: string;
27
+ promptGuidelines?: string[];
28
+ }
29
+ export type PiToolExtrasConfig = Partial<Record<AcpToolName, PiToolExtras>>;
30
+ export interface PiAdapterSurface {
31
+ promptSections: Partial<PiPromptSections>;
32
+ toolExtras: PiToolExtrasConfig;
33
+ }
34
+ /**
35
+ * Pi-specific part of a pack. `surface.adapters.pi` is opaque to the kernel,
36
+ * so the adapter sanitizes it here: tri-state prompt sections restricted to
37
+ * pi's section keys, per-tool extras with malformed fields dropped.
38
+ */
39
+ export declare function piAdapterSurface(pack: Pack): PiAdapterSurface;
40
+ export interface InlineSurface {
41
+ prompts?: Partial<Prompts>;
42
+ promptSections?: Partial<PiPromptSections>;
43
+ nudgeSections?: NudgeSectionsConfig;
44
+ toolPrompts?: ToolPromptsConfig;
45
+ }
46
+ export interface MergedSurface {
47
+ prompts: Partial<Prompts>;
48
+ promptSections: Partial<PiPromptSections>;
49
+ nudgeSections: NudgeSectionsConfig;
50
+ toolPrompts: ToolPromptsConfig;
51
+ }
52
+ export declare function mergeSurface(pack: Pack | null, inline: InlineSurface): MergedSurface;
53
+ export declare function readToolSurfaceWithPacks(cwd: string): ToolPromptsConfig;
@@ -0,0 +1,21 @@
1
+ export declare function isBiliProxyBaseUrl(baseUrl: string | undefined): boolean;
2
+ /**
3
+ * Shown (and logged) when the extension detects that the model's baseUrl
4
+ * routes through the billion-context wire proxy and stands down. The proxy
5
+ * runs compression server-side and owns the ref coordinate space, so running
6
+ * ACP in-process on top of it would double-compress every request.
7
+ */
8
+ export declare const PROXY_STAND_DOWN_MESSAGE: string;
9
+ /**
10
+ * Shown (and logged) when a billion-context host-native entry owns this
11
+ * process (issue #461): the native entry self-spawns an in-process proxy and
12
+ * rewrites model API traffic at the fetch layer, so running ACP in-process on
13
+ * top of it would double-compress every request. Its BILLION_CONTEXT_PROXY env
14
+ * var is written only after the proxy is up (past the synchronous extension
15
+ * load), and the fetch-layer rewrite keeps the configured baseUrl clean —
16
+ * BILLION_CONTEXT_NATIVE, set synchronously at module evaluation by the native
17
+ * entry (billion-context markNativeHost, #820/#824), is the only signal visible
18
+ * at our checkpoints. `host` is the marker value ("pi", "opencode", …) — kept
19
+ * in the text so a mismatched host is diagnosable from the warning alone.
20
+ */
21
+ export declare function nativeStandDownMessage(host: string): string;
@@ -0,0 +1,68 @@
1
+ import type { SessionMessageEntry } from "@earendil-works/pi-coding-agent";
2
+ type AgentMessage = SessionMessageEntry["message"];
3
+ /** [#336] Config for dropping oversized reasoning (thinking) parts from
4
+ * historical `compress` tool calls — exact alignment with opencode-acp #377.
5
+ * `compress` calls are hard-exempt from compression (their tool results are
6
+ * the anchors that keep block summaries addressable), so their thinking
7
+ * rides along every request as an unreclaimable context floor. This pass
8
+ * removes `thinking` parts at request time (persisted history is never
9
+ * modified) from closed-round compress messages whose total reasoning length
10
+ * exceeds `threshold`. A round is closed on ROUND EVIDENCE, not on user
11
+ * messages: the compress tool result must have arrived and at least one
12
+ * message must exist after it. The in-flight round (result still missing or
13
+ * still the last message) is never touched [#348]. */
14
+ export interface CompressReasoningConfig {
15
+ /** Master switch. Default: true. `drop: false` disables the pass entirely
16
+ * (kill-switch; also the recipe for providers whose thinking items are
17
+ * opaque and must round-trip unmodified, e.g. set it per-provider under
18
+ * `compress.providers.<name>`). */
19
+ drop?: boolean;
20
+ /** Single-thinking size gate (chars): a closed-turn compress message's
21
+ * total reasoning length (summed across parts of that message) must
22
+ * STRICTLY EXCEED this to be dropped. Small thinkings are kept; lengths
23
+ * are NOT accumulated across messages. Default: 2048. `0` drops any
24
+ * non-empty reasoning (only zero-length reasoning survives). */
25
+ threshold?: number;
26
+ }
27
+ export declare const DEFAULT_COMPRESS_REASONING: Required<CompressReasoningConfig>;
28
+ export declare function resolveReasoningDrop(cfg?: CompressReasoningConfig): Required<CompressReasoningConfig>;
29
+ /** [#361] Strict-echo thinking upstreams (DeepSeek thinking mode) require
30
+ * `reasoning_content` to round-trip verbatim across tool-call turns; a rebuilt
31
+ * request whose closed-round assistant messages lost their reasoning is rejected
32
+ * with HTTP 400 ("reasoning_content ... must be passed back"). Detect statically
33
+ * from the model's configured origin/name so the request-time drop pass stands
34
+ * down. Mirrors the proxy-side billion-context#690 detector, adapted to pi's
35
+ * signal sources: the in-process adapter does not own the HTTP layer, so there
36
+ * is no learn-on-400 self-heal here — see CONFIGURATION.md for the manual
37
+ * `drop:false` escape hatch (GLM-thinking / QwQ / self-hosted mirrors). */
38
+ export declare function isStrictReasoningEcho(provider?: string, baseUrl?: string): boolean;
39
+ /** [#361] Force the drop pass off on a strict-echo upstream so its reasoning
40
+ * round-trips unmodified. Pure: returns cfg unchanged when not strict-echo or
41
+ * already disabled. Cost-free for non-thinking models on those hosts — they emit
42
+ * no `thinking` parts, so the pass would be a no-op regardless. */
43
+ export declare function applyStrictReasoningGate(cfg: Required<CompressReasoningConfig>, provider?: string, baseUrl?: string): Required<CompressReasoningConfig>;
44
+ export declare function countThinkingChars(messages: AgentMessage[]): number;
45
+ /** Request-time pass aligned with opencode-acp #377: remove `thinking` parts
46
+ * from a message only when ALL gates hold —
47
+ * 1. closed round [#348]: EVERY `compress` toolCall in the message has its
48
+ * tool-result message (role `toolResult`, matching `toolCallId`) at a
49
+ * later index, and at least one message exists after that result (the
50
+ * round has demonstrably moved on). A compress call without a result, or
51
+ * whose result is still the last message, is in flight and never touched
52
+ * — no user message is required, so long agentic sessions do close
53
+ * rounds; a synthetic nudge pushed later cannot retroactively close one;
54
+ * 2. selector: the message carries a `toolCall` part with name "compress"
55
+ * (only compress; other protected tools would need their own explicit
56
+ * config);
57
+ * 3. size: the message's total reasoning length strictly exceeds
58
+ * `threshold` chars (summed across parts, never across messages).
59
+ * Pure: never mutates the input; idempotent; fail-safe (any error returns
60
+ * the input unchanged). */
61
+ export declare function dropCompressReasoning(messages: AgentMessage[], cfg?: CompressReasoningConfig): AgentMessage[];
62
+ /** Lean: thinking that stays on the wire for compress-call messages.
63
+ * estimateTokens skips compress cores whole, and the projection hangs a
64
+ * message's thinking on its FIRST tool-call core, so count thinking only for
65
+ * messages whose first call is compress, after the same drop pass the
66
+ * request applies (closed, oversized rounds lose it). */
67
+ export declare function keptCompressThinkingTokens(messages: AgentMessage[], cfg?: CompressReasoningConfig): number;
68
+ export {};
@@ -0,0 +1,8 @@
1
+ import { Type } from "typebox";
2
+ import type { ToolDefinition } from "@earendil-works/pi-coding-agent";
3
+ import type { AcpRuntime } from "./runtime.js";
4
+ declare const RuleParams: Type.TObject<{
5
+ rule: Type.TOptional<Type.TString>;
6
+ }>;
7
+ export declare function makeRuleTool(runtime: AcpRuntime): ToolDefinition<typeof RuleParams>;
8
+ export {};
@@ -0,0 +1,199 @@
1
+ import type { ExtensionContext, SessionEntry, SessionMessageEntry } from "@earendil-works/pi-coding-agent";
2
+ import { type CompressionCore, type CompressionState, type Config, type Prompts } from "acp-kernel";
3
+ import { type AdapterConfig } from "./config.js";
4
+ import { type CompressReasoningConfig } from "./reasoning-drop.js";
5
+ import type { SentViewMeterRecord } from "./tokens.js";
6
+ import { entriesToCoreMessages } from "./messages.js";
7
+ import { SessionStateStore } from "./state.js";
8
+ import { ThrottleEpisode } from "./throttle-retry.js";
9
+ import { OverflowEpisode } from "./overflow-selfheal.js";
10
+ import { type TurnBoundaryPolicy } from "./turn-boundary.js";
11
+ type AgentMessage = SessionMessageEntry["message"];
12
+ export declare function readContextEntries(sm: ExtensionContext["sessionManager"]): SessionEntry[];
13
+ export declare function isPiHost(sm: ExtensionContext["sessionManager"]): boolean;
14
+ /** #453: key for the compress-retry circuit breaker, derived from PERSISTED
15
+ * entries only. Live-merged tail ids (content-addressed since #459) can still
16
+ * churn when the host's view of a not-yet-persisted message drifts, so a
17
+ * breaker keyed on the merged view could reset failCount mid-episode (#452
18
+ * log: cap → inject loop). Persisted entry ids are immutable; this key
19
+ * changes only when a genuine new user message reaches the session log.
20
+ * pi-native hosts see no change. */
21
+ export declare function retryBreakerKey(sm: ExtensionContext["sessionManager"], policy?: TurnBoundaryPolicy): string | undefined;
22
+ /** Minimal identity of a session for state operations that don't need a live
23
+ * ExtensionContext (hosts deriving inline child sessions build these from
24
+ * whatever session handles they hold). */
25
+ export interface SessionRef {
26
+ sessionId: string;
27
+ sessionFile?: string;
28
+ }
29
+ export interface AcpRuntime {
30
+ core: CompressionCore;
31
+ /** Set when the host is unsupported (currently: OMP / oh-my-pi) or when the
32
+ * model's baseUrl routes through the billion-context wire proxy (#296).
33
+ * Once true, the extension stands down: the context transform, system-prompt
34
+ * injection, compaction-cancel and ACP tools all no-op so the host runs
35
+ * untouched. Set at session_start (first context event as fallback). */
36
+ refused: boolean;
37
+ /** User-facing reason shown when a refused ACP tool is invoked; null means
38
+ * the default OMP refusal text applies. Set together with `refused`. */
39
+ refusalMessage: string | null;
40
+ /** Per-session provider-throttle retry episode (attempt budget + kick
41
+ * pacing), keyed by session id so concurrent sessions in one extension
42
+ * instance cannot share an episode. Reset on session_start and on any
43
+ * real progress / user input. */
44
+ throttleFor: (sid: string) => ThrottleEpisode;
45
+ /** Drop a session's throttle episode entirely (session_shutdown): aborts a
46
+ * pending kick sleep and releases the map entry so a long-lived process
47
+ * that cycles through many sessions doesn't accumulate them. */
48
+ throttleDrop: (sid: string) => void;
49
+ /** Per-session tokenCount scale tracker (estimate vs provider). Returns true
50
+ * when the scale just flipped (stale↔not-stale) so the caller can reset the
51
+ * growth baseline — a cross-scale delta is a false artifact, not real growth
52
+ * (issue #267). The first observation for a session never reports a flip. */
53
+ noteTokenScale: (sid: string, stale: boolean) => boolean;
54
+ /** Drop a session's token-scale tracker (session_shutdown). */
55
+ dropTokenScale: (sid: string) => void;
56
+ store: SessionStateStore;
57
+ adapter: AdapterConfig;
58
+ setAdapter(adapter: AdapterConfig): void;
59
+ prompts: Prompts;
60
+ setPrompts(prompts: Prompts): void;
61
+ markNudgeShown(sid: string, turnKey: string, tokenCount?: number): void;
62
+ nudgeShownFor(sid: string, turnKey: string): boolean;
63
+ /** tokenCount at the last actual nudge injection for this turn, for growth-aware re-inject (issue #269). */
64
+ nudgeShownTokensFor(sid: string, turnKey: string): number | undefined;
65
+ /** Clears the token-count stamps recorded by markNudgeShown — used on a token-scale flip (issue #267) so the same-turn re-inject floor (#269 / PR #316) is not computed against an old-scale stamp. */
66
+ clearNudgeTokenStamps(sid: string): void;
67
+ /** Like the nudgeShown* pair but tracks the persisted display-only session
68
+ * entry (issue #326): at most one record per user turn even when the
69
+ * emergency nudge re-injects on every LLM call. */
70
+ markNudgeRecorded(sid: string, turnKey: string): void;
71
+ nudgeRecordedFor(sid: string, turnKey: string): boolean;
72
+ /** Process compress toolResults for the CURRENT user turn only (the caller
73
+ * scopes the list — see collectCompressOutcomes in src/index.ts); idempotent
74
+ * per toolCallId. turnKey MUST be the stable persisted-boundary key
75
+ * (retryBreakerKey) — a key derived from live-merged entries churns under
76
+ * fork hosts and resets the counter mid-episode (#453). Outcome classes:
77
+ * isError or noop (0-block panel) → failure (count++), success panel
78
+ * (>= 1 block) → reset, other non-error text → neutral (count unchanged).
79
+ * Returns the failure count and whether the cap was just reached. */
80
+ noteCompressOutcomes(sid: string, turnKey: string, outcomes: ReadonlyArray<{
81
+ toolCallId: string;
82
+ isError: boolean;
83
+ success: boolean;
84
+ noop?: boolean;
85
+ }>): {
86
+ count: number;
87
+ cappedNow: boolean;
88
+ };
89
+ /** True when this turn already burned MAX_COMPRESS_ATTEMPTS failed/no-op
90
+ * compress calls — used to stop re-injecting the (dedup-exempt) emergency
91
+ * nudge that would otherwise keep looping no-op compressions (issue #6). */
92
+ compressRetryCappedFor(sid: string, turnKey: string): boolean;
93
+ clearNudgeTracking(sid: string): void;
94
+ clearCompressRetryTracking(sid: string): void;
95
+ liveContextLimit(ctx: ExtensionContext): number;
96
+ configFor(ctx: ExtensionContext): Config;
97
+ /** [#336] Effective compress.reasoning drop settings for the active model
98
+ * (three-level merge + defaults). Feeds the request-time pass in the
99
+ * context transform. */
100
+ reasoningDropFor(ctx: ExtensionContext): Required<CompressReasoningConfig>;
101
+ /** Effective historical-image strip policy for the active model (issue #321).
102
+ * Host-side policy — deliberately NOT part of the kernel Config object. */
103
+ stripImagesFor(ctx: ExtensionContext): {
104
+ enabled: boolean;
105
+ keepRecent: number;
106
+ };
107
+ /** Re-read ~/.<dir>/acp.json + <cwd>/<dir>/acp.json and re-derive the adapter
108
+ * config when the contents change. Cheap no-op when unchanged. Called at
109
+ * session_start and on every context event so config edits apply live. */
110
+ reloadConfig(cwd: string): Promise<void>;
111
+ stateFor(ctx: ExtensionContext, liveMessages?: AgentMessage[]): Promise<{
112
+ state: CompressionState;
113
+ coreMessages: ReturnType<typeof entriesToCoreMessages>;
114
+ entries: SessionEntry[];
115
+ }>;
116
+ /** Record the EXACT sent view measured off the real processTurn output for
117
+ * this turn (issue #561): `viewTokens` counts turn.messages (the pruned,
118
+ * summary-injected projection that actually went on the wire), and `usable`
119
+ * marks whether that count is an honest view — a turn that ran with
120
+ * tokenCount at/above the truncate band may have truncated its output, so
121
+ * its post-truncation size under-reports and must not be trusted next turn. */
122
+ noteSentViewCount(sid: string, record: SentViewMeterRecord): void;
123
+ /** Last turn's measured sent view, or undefined on the first turn / after a
124
+ * signature change (blocks added or dropped, window re-centered). */
125
+ peekSentViewCount(sid: string): SentViewMeterRecord | undefined;
126
+ /** Drop a session's sent-view meter (session_shutdown). */
127
+ dropSentViewCount(sid: string): void;
128
+ /** Drop a session's incremental projection cache (session_shutdown): it
129
+ * holds the full projected CoreMessage[] for the session history. */
130
+ dropProjectionCache(sid: string): void;
131
+ save(state: CompressionState, ctx: ExtensionContext): Promise<void>;
132
+ /** #364 inline child sessions (same process, e.g. Prime RLM): derive the
133
+ * child's compression state from another session's. Inherits blocks /
134
+ * message refs / token snapshot so decompress + search_context keep working
135
+ * on inherited blocks; resets every rhythm ledger (nudge cadence, stats,
136
+ * absorb). One-time: writes a derivation marker into the child sidecar and
137
+ * refuses to run again; also refuses when the child already owns real
138
+ * (non-derived) blocks or when the parent has no blocks. Separate-process
139
+ * pi-native delegates must NOT call this — their parentSession header
140
+ * already inherits verbatim. Returns true when the child state was derived. */
141
+ deriveChildState(child: SessionRef, parent: SessionRef): Promise<boolean>;
142
+ acquireLock(sid: string): Promise<() => void>;
143
+ /** Per-session overflow self-heal state (learned window + armed emergency).
144
+ * Keyed by session id so concurrent sessions cannot share an episode. */
145
+ overflowFor(sid: string): OverflowEpisode;
146
+ /** Drop a session's overflow episode entirely (session_shutdown): releases
147
+ * the map entry so a long-lived process cycling through many sessions
148
+ * doesn't accumulate them. */
149
+ overflowDrop(sid: string): void;
150
+ /** Record a compress call whose every requested range is dead (refs stale or
151
+ * unknown — the kernel cannot create a block from it no matter what summary
152
+ * is written). Returns the failure count for this exact range fingerprint
153
+ * in this session (issue #250 loop breaker). */
154
+ noteDeadCompress(sid: string, fingerprint: string): number;
155
+ /** Drop a session's dead-range repeat tracking (a successful compress is
156
+ * progress — reset the #250 loop breaker for the next attempt round;
157
+ * session_shutdown for memory hygiene). Compress does NOT renumber refs. */
158
+ clearDeadCompress(sid: string): void;
159
+ /** Dedicated summary model ref (`session`, `provider/id`, bare id), or undefined. */
160
+ getCompressionModelRef(): string | undefined;
161
+ /** Persist the ref to the global acp.json (null clears) and apply it now. */
162
+ setCompressionModelRef(value: string | null): Promise<void>;
163
+ /** Exact transformed messages from the latest provider-bound context pass. */
164
+ setLastSentMessages(sid: string, messages: AgentMessage[]): void;
165
+ getLastSentMessages(sid: string): AgentMessage[] | null;
166
+ dropLastSentMessages(sid: string): void;
167
+ /** Record one turn's FRESH-anchor provider usage sample and report whether
168
+ * the recent window is stable enough to calibrate the internal estimate
169
+ * against it (issue #455): >=3 of the last 4 samples agree within a 25%
170
+ * spread. A jittering getContextUsage() must not become a moving cap, so
171
+ * unstable windows report false and the raw estimate stands. */
172
+ noteHostUsage(sid: string, tokens: number): boolean;
173
+ /** Drop a session's host-usage stability window (session_shutdown). */
174
+ dropHostUsageSamples(sid: string): void;
175
+ /** Track persistent >2x internal-vs-provider size disagreement (issue #455);
176
+ * returns true exactly once per episode — on the third consecutive
177
+ * divergent turn — so the caller warns once instead of every turn. A
178
+ * convergent turn ends the episode. */
179
+ noteSizeDivergence(sid: string, divergent: boolean): boolean;
180
+ /** Drop a session's size-divergence episode (session_shutdown). */
181
+ dropSizeDivergence(sid: string): void;
182
+ /** Track the kernel's terminal-escape signal (issue #464): returns true
183
+ * exactly once per episode — the first consecutive stuck fire — so the
184
+ * caller logs/notifies once instead of every turn. A non-escaping turn
185
+ * ends the episode. */
186
+ noteTerminalEscape(sid: string, active: boolean): boolean;
187
+ /** Drop a session's terminal-escape episode (session_shutdown). */
188
+ dropTerminalEscape(sid: string): void;
189
+ /** Track the kernel's truncation-skipped signal (issue #464): returns true
190
+ * exactly once per episode for low-frequency diagnostics. A turn that
191
+ * truncates (or skips nothing) ends the episode. */
192
+ noteTruncationSkipped(sid: string, active: boolean): boolean;
193
+ /** Drop a session's truncation-skipped episode (session_shutdown). */
194
+ dropTruncationSkipped(sid: string): void;
195
+ }
196
+ /** Max FAILED compress calls per user turn before the nudge circuit breaker engages. */
197
+ export declare const MAX_COMPRESS_ATTEMPTS = 3;
198
+ export declare function createRuntime(adapter: AdapterConfig): AcpRuntime;
199
+ export {};
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Search index — bridges pi's session log into acp-kernel's search.
3
+ *
4
+ * Builds SearchDoc[] from:
5
+ * 1. All compression blocks (active AND inactive) — via blockDocs()
6
+ * 2. Historical messages that compression folded into a block summary.
7
+ *
8
+ * Which messages are searchable? Those covered by SOME block's
9
+ * effectiveMessageIds — i.e. messages that were compressed into a summary and
10
+ * are no longer individually visible. Messages still live in context (not in
11
+ * any block) are skipped: the model can already see them.
12
+ *
13
+ * We deliberately do NOT use pi's buildContextEntries for the visible check:
14
+ * ACP prunes messages itself (no pi `compaction` entry is written), so pi
15
+ * reports ALL entries as in-context. The ACP state is the source of truth.
16
+ */
17
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
18
+ import { type SearchDoc } from "acp-kernel";
19
+ import type { CompressionState } from "acp-kernel";
20
+ export declare function buildSearchDocs(ctx: ExtensionContext, state: CompressionState): SearchDoc[];