pi-openai-toolkit 0.20.7 → 0.21.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 (55) hide show
  1. package/LICENSE +21 -21
  2. package/NOTICE +83 -83
  3. package/README.md +8 -26
  4. package/README.zh.md +8 -26
  5. package/config.schema.json +278 -366
  6. package/docs/configuration.md +294 -310
  7. package/docs/internals.md +166 -175
  8. package/package.json +3 -13
  9. package/src/compact-client.ts +412 -389
  10. package/src/compaction-errors.ts +96 -0
  11. package/src/compaction-projection.ts +35 -35
  12. package/src/config/legacy.ts +519 -717
  13. package/src/config/migration.ts +103 -100
  14. package/src/config/notifications.ts +36 -28
  15. package/src/config/policy.ts +10 -18
  16. package/src/config/v2.ts +1167 -1380
  17. package/src/config.ts +310 -319
  18. package/src/context-management/codex-provider.ts +270 -270
  19. package/src/context-management/history-notes.ts +357 -357
  20. package/src/context-management/manual-compact.ts +0 -11
  21. package/src/context-management/tools.ts +371 -371
  22. package/src/context-management/types.ts +168 -168
  23. package/src/context-management/window-manager.ts +801 -801
  24. package/src/debug.ts +238 -237
  25. package/src/deferred-tool-carryover.ts +282 -282
  26. package/src/details-store.ts +151 -151
  27. package/src/extension-runtime.ts +1662 -1623
  28. package/src/image-generation/extension.ts +189 -189
  29. package/src/image-generation/protocol.ts +579 -579
  30. package/src/image-generation/types.ts +300 -300
  31. package/src/native-fallback.ts +4 -132
  32. package/src/payload-rewrite.ts +515 -518
  33. package/src/pi-context-hook.ts +117 -117
  34. package/src/remote-v2-client.ts +811 -814
  35. package/src/request-context-cache.ts +155 -155
  36. package/src/responses-headers.ts +180 -180
  37. package/src/runtime.ts +865 -832
  38. package/src/serializer.ts +882 -882
  39. package/src/supported-environment.ts +27 -27
  40. package/src/types.ts +565 -648
  41. package/src/web-search/alpha-client.ts +876 -876
  42. package/src/web-search/extension.ts +530 -530
  43. package/src/web-search/payload.ts +299 -299
  44. package/src/web-search/prompt.ts +60 -60
  45. package/src/web-search/types.ts +166 -166
  46. package/tsconfig.check.json +50 -54
  47. package/src/auto-mode/classifier.ts +0 -161
  48. package/src/auto-mode/extension.ts +0 -691
  49. package/src/auto-mode/model-selection-guard.ts +0 -52
  50. package/src/auto-mode/policy.ts +0 -146
  51. package/src/auto-mode/prompt.ts +0 -279
  52. package/src/auto-mode/reviewer.ts +0 -325
  53. package/src/auto-mode/tool-review-tui.ts +0 -211
  54. package/src/auto-mode/transcript.ts +0 -135
  55. package/src/auto-mode/types.ts +0 -137
package/src/types.ts CHANGED
@@ -1,648 +1,565 @@
1
- import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
- import type { ConfigDocumentSnapshot } from "./config/policy";
3
- import type { CompactionEntry, CompactionResult, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
4
-
5
- export const TOOLKIT_ID = "pi-openai-toolkit";
6
- export const COMPACTION_EXTENSION_ID = `${TOOLKIT_ID}:compaction`;
7
- export const WEB_SEARCH_EXTENSION_ID = `${TOOLKIT_ID}:web-search`;
8
- export const IMAGE_GENERATION_EXTENSION_ID = `${TOOLKIT_ID}:image-generation`;
9
- export const AUTO_MODE_EXTENSION_ID = `${TOOLKIT_ID}:auto-mode`;
10
- export const DEFAULT_ARTIFACT_ROOT = "~/.pi/agent/artifacts/pi-openai-toolkit/compaction";
11
- export const REDACTED_VALUE = "[REDACTED]";
12
- /**
13
- * APIs the extension knows how to build a `/responses/compact` URL for.
14
- * `compaction.responsesApis` in config.json may only narrow this set.
15
- */
16
- export const RESPONSES_COMPACT_CAPABLE_APIS = [
17
- "openai-responses",
18
- "openai-codex-responses",
19
- "azure-openai-responses",
20
- ] as const;
21
- /**
22
- * Bare model ID used by the independent Astra effort compatibility layer.
23
- * Remote Context eligibility is API/provider based with an exact gateway opt-in;
24
- * this constant does not verify backend support or grant that opt-in.
25
- */
26
- export const ASTRA_MODEL_ID = "gpt-6-astra";
27
- export const LEGACY_NATIVE_COMPACTION_STRATEGY = "openai-native-compact-v1";
28
- export const REMOTE_V2_COMPACTION_STRATEGY = "openai-remote-compaction-v2";
29
- export const NATIVE_COMPACTION_STRATEGY = REMOTE_V2_COMPACTION_STRATEGY;
30
- /**
31
- * Provenance recorded when the checkpoint input came from Pi's ordered context
32
- * hook projection. Older persisted entries may omit it and must not be
33
- * replayed by Remote V2.
34
- */
35
- export const NATIVE_COMPACTION_INPUT_PROVENANCE = "pi-context-hook-v1" as const;
36
- /** Explicit opt-in marker for the pre-projection raw session/branch chain. */
37
- export const LEGACY_REMOTE_V2_INPUT_PROVENANCE = "legacy-raw-context-v1" as const;
38
- export type NativeCompactionInputProvenance =
39
- | typeof NATIVE_COMPACTION_INPUT_PROVENANCE
40
- | typeof LEGACY_REMOTE_V2_INPUT_PROVENANCE;
41
- /**
42
- * Pi currently requires CompactionResult.summary to be text. This marker is only a
43
- * replay shim; the provider receives the opaque item from details.compactedWindow.
44
- */
45
- export const NATIVE_COMPACTION_FALLBACK_SUMMARY = "[OpenAI remote v2 opaque compaction checkpoint]";
46
-
47
- export const THINKING_LEVELS: readonly ThinkingLevel[] = [
48
- "off",
49
- "minimal",
50
- "low",
51
- "medium",
52
- "high",
53
- "xhigh",
54
- "max",
55
- ];
56
-
57
- export type DebugArtifactKind =
58
- | "provider-request"
59
- | "compact-response"
60
- | "compaction-event"
61
- | "lifecycle";
62
-
63
- export type ContextManagementMode = "off" | "remote";
64
-
65
- /** How Remote V2 obtains the history it sends to the compaction endpoint. */
66
- export type RemoteV2ContextSource = "pi-context-hook" | "legacy";
67
-
68
- /** Native-method fallback compaction: which model runs pi's compact() and how deeply. */
69
- export type NativeFallbackConfig = {
70
- enabled: boolean;
71
- /**
72
- * "provider/model-id" used for native-method fallback compaction when the active model
73
- * cannot use remote v2 at all. A failed remote request is instead summarized by
74
- * `remoteCompactModel`; the two sources never substitute for each other. Unset means the
75
- * active model via pi's default path.
76
- */
77
- model?: string;
78
- /** Thinking level passed to pi's native compact() when the fallback model runs. */
79
- thinkingLevel: ThinkingLevel;
80
- };
81
-
82
- /**
83
- * Legacy compatibility setting. Remote Context history is no longer bulk-compacted when
84
- * selecting a non-window model; local projection and a no-summary boundary own that handoff.
85
- */
86
- export type LeaveManagedModePolicy = "warn" | "compact";
87
-
88
- export type DebugConfig = {
89
- debug: boolean;
90
- notifyOnLoad: boolean;
91
- logProviderPayloads: boolean;
92
- logCompactResponses: boolean;
93
- redactSensitiveData: boolean;
94
- artifactRoot: string;
95
- };
96
-
97
- export type CompactionConfig = {
98
- enabled: boolean;
99
- /**
100
- * Legacy compatibility setting. Non-window consumers never receive retired Remote Context
101
- * history, regardless of this value; retained config files continue to parse it.
102
- */
103
- leaveManagedMode: LeaveManagedModePolicy;
104
- /** Optional Codex Remote Context management. Disabled by default for compatibility. */
105
- contextManagement: ContextManagementMode;
106
- /**
107
- * Allow a Responses session whose latest compaction was not created by this extension
108
- * to restart native compaction from Pi's current serialized session context.
109
- */
110
- allowCompactionContinuityBreak: boolean;
111
- /**
112
- * "provider/model-id" used only for remote_compaction_v2. The active session model
113
- * remains the checkpoint consumer and continues handling normal requests.
114
- */
115
- remoteCompactModel?: string;
116
- /**
117
- * Remote V2 input source. Pi's patched context hook preserves the normal
118
- * provider-visible projection; legacy keeps the pre-patch raw session/branch chain.
119
- */
120
- remoteV2ContextSource: RemoteV2ContextSource;
121
- /** Native-method fallback compaction policy (non-Responses APIs, or when the compact endpoint fails). */
122
- nativeFallback: NativeFallbackConfig;
123
- /**
124
- * Subset of RESPONSES_COMPACT_CAPABLE_APIS that should use remote compaction.
125
- */
126
- responsesApis: string[];
127
- /**
128
- * Exact "provider/model" keys allowed to use Codex Remote Context on the
129
- * `openai-responses` gateway wire. The native `openai-codex` route ignores
130
- * this list; gateway coverage is opt-in per model.
131
- */
132
- gatewayContextModels: string[];
133
- /**
134
- * Percentage of the context window that, when remaining tokens drop below it,
135
- * triggers a checkpoint/new_context reminder (0-100; 0 disables reminders).
136
- */
137
- contextReminderThresholdPercent: number;
138
- notifyOnLoad: boolean;
139
- debug: boolean;
140
- logProviderPayloads: boolean;
141
- logCompactResponses: boolean;
142
- redactSensitiveData: boolean;
143
- artifactRoot: string;
144
- };
145
-
146
- export type WebSearchRoute = "local" | "hosted" | "standalone-alpha";
147
-
148
- export type WebSearchConfig = {
149
- enabled: boolean;
150
- /** Exact provider/model keys allowed to use toolkit-native Web Search. */
151
- models: string[];
152
- /** Explicit route for every model not matched by `routes`. */
153
- defaultRoute?: WebSearchRoute;
154
- /** Exact `provider/model-id` overrides; no globs or fuzzy matching. */
155
- routes?: Record<string, WebSearchRoute>;
156
- };
157
-
158
- export type ImageGenerationConfig = {
159
- enabled: boolean;
160
- /**
161
- * Bare Images API model ids this toolkit may send, without a `provider/` prefix.
162
- * The first legacy entry is the default; `openai_generate_image`'s optional `model` argument
163
- * may select any other entry in this order-preserving list.
164
- */
165
- models: string[];
166
- };
167
-
168
- /** `side-effect` reviews bash/write/edit plus extras; `all` reviews every tool call. */
169
- export type AutoModeGate = "side-effect" | "all";
170
-
171
- export type AutoModeConfig = {
172
- enabled: boolean;
173
- /** Exact provider/model keys whose sessions may engage auto mode. */
174
- models: string[];
175
- /** "provider/model-id" that approves gated tool calls while auto mode is engaged. */
176
- reviewerModel?: string;
177
- gate: AutoModeGate;
178
- /** Extra tool names reviewed on top of the side-effect default set. */
179
- extraTools: string[];
180
- /** Reviewer deadline in ms; expiry degrades to human confirmation or fail-closed. */
181
- timeoutMs: number;
182
- /** Give the reviewer a bounded transcript so it can judge user authorization. */
183
- transcript: boolean;
184
- /** Let the reviewer spend read-only tool calls establishing facts before deciding. */
185
- evidenceTools: boolean;
186
- maxEvidenceRounds: number;
187
- /** Non-blocking trajectory pre-scorer that can satisfy ordinary gated calls. */
188
- classifier: AutoModeClassifierConfig;
189
- /** End a turn that keeps getting denied instead of negotiating forever. */
190
- circuitBreaker: AutoModeCircuitBreakerConfig;
191
- };
192
-
193
- export type AutoModeClassifierConfig = {
194
- enabled: boolean;
195
- /** "provider/model-id"; falls back to `autoMode.reviewerModel` when unset. */
196
- model?: string;
197
- timeoutMs: number;
198
- /** How many tool calls may elapse between scoring and use before a score is stale. */
199
- maxLag: number;
200
- };
201
-
202
- export type AutoModeCircuitBreakerConfig = {
203
- /** Consecutive reviewer denials in one turn that end the turn. 0 disables. */
204
- consecutiveDenials: number;
205
- /** Denials inside `windowSize` reviewed calls that end the turn. 0 disables. */
206
- recentDenials: number;
207
- windowSize: number;
208
- };
209
-
210
- export const REVIEWER_TIMEOUT_MIN_MS = 1_000;
211
- export const REVIEWER_TIMEOUT_MAX_MS = 120_000;
212
-
213
- /** Read-only investigation rounds the reviewer may spend before it must answer. */
214
- export const EVIDENCE_ROUNDS_MIN = 0;
215
- export const EVIDENCE_ROUNDS_MAX = 8;
216
-
217
- export const CLASSIFIER_MAX_LAG_MIN = 0;
218
- export const CLASSIFIER_MAX_LAG_MAX = 20;
219
-
220
- export const DEFAULT_CLASSIFIER_TIMEOUT_MS = 15_000;
221
-
222
- /** Denial counts are small integers; 0 disables the matching trigger. */
223
- export const BREAKER_LIMIT_MIN = 0;
224
- export const BREAKER_LIMIT_MAX = 100;
225
- export const BREAKER_WINDOW_MIN = 1;
226
- export const BREAKER_WINDOW_MAX = 200;
227
-
228
- export type ToolkitConfig = {
229
- compaction: CompactionConfig;
230
- webSearch: WebSearchConfig;
231
- imageGeneration: ImageGenerationConfig;
232
- autoMode: AutoModeConfig;
233
- };
234
-
235
- export type LoadedToolkitConfig = {
236
- /** Legacy normalization seam. Feature entrypoints must resolve this snapshot for their model. */
237
- config: ToolkitConfig;
238
- /** Optional only for existing injected legacy readers; real loads always include it. */
239
- document?: ConfigDocumentSnapshot;
240
- /** Path of the config file that was applied, if it existed and parsed. */
241
- source?: string;
242
- warnings: string[];
243
- };
244
-
245
- export type ArtifactPaths = {
246
- rootDir: string;
247
- sessionDir: string;
248
- providerRequestsDir: string;
249
- compactResponsesDir: string;
250
- compactionDir: string;
251
- lifecycleDir: string;
252
- };
253
-
254
- export type ArtifactSessionInfo = {
255
- cwd: string;
256
- sessionId?: string;
257
- sessionFile?: string;
258
- sessionDir?: string;
259
- };
260
-
261
- export type ArtifactContext = ArtifactSessionInfo | Pick<ExtensionContext, "cwd" | "sessionManager">;
262
-
263
- export type DebugArtifactEnvelope = {
264
- extension: string;
265
- kind: DebugArtifactKind;
266
- timestamp: string;
267
- cwd: string;
268
- sessionId?: string;
269
- sessionFile?: string;
270
- sessionDir?: string;
271
- redaction: {
272
- enabled: boolean;
273
- };
274
- data: unknown;
275
- };
276
-
277
- export type RedactOptions = {
278
- placeholder?: string;
279
- };
280
-
281
- export type NativeCompactionStrategy =
282
- | typeof LEGACY_NATIVE_COMPACTION_STRATEGY
283
- | typeof REMOTE_V2_COMPACTION_STRATEGY;
284
-
285
- export type NativeCompactionRequestMeta = {
286
- tokensBefore?: number;
287
- previousSummaryPresent?: boolean;
288
- };
289
-
290
- export type NativeCompactionIdentity = {
291
- provider: string;
292
- api: string;
293
- model: string;
294
- baseUrl: string;
295
- };
296
-
297
- export const CACHE_STACK_ACTIVATION_ENTRY_TYPE = "pi-cache-stack.activation-state.v1" as const;
298
-
299
- export type DeferredToolCarryoverV1 = {
300
- version: 1;
301
- source: typeof CACHE_STACK_ACTIVATION_ENTRY_TYPE;
302
- toolNames: string[];
303
- catalogHash?: string;
304
- };
305
-
306
- export type NativeCompactionDetails = NativeCompactionIdentity & {
307
- strategy: NativeCompactionStrategy;
308
- /**
309
- * Input source recorded for checkpoints created by this extension. Older
310
- * persisted entries may omit it and must not be treated as verified for
311
- * Remote V2 replay.
312
- */
313
- inputProvenance?: NativeCompactionInputProvenance;
314
- /** Actual producer of the opaque checkpoint; absent on legacy same-model entries. */
315
- compactionModel?: NativeCompactionIdentity;
316
- /** Cache-stack activation state captured at this opaque checkpoint. */
317
- deferredToolCarryover?: DeferredToolCarryoverV1;
318
- compactedWindow: unknown[];
319
- compactResponseId?: string;
320
- createdAt: string;
321
- requestMeta?: NativeCompactionRequestMeta;
322
- };
323
-
324
- export type NativeCompactionEntry = CompactionEntry<NativeCompactionDetails>;
325
-
326
- export type CreateNativeCompactionDetailsInput = NativeCompactionIdentity & {
327
- inputProvenance: NativeCompactionInputProvenance;
328
- compactionModel?: NativeCompactionIdentity;
329
- deferredToolCarryover?: DeferredToolCarryoverV1;
330
- compactedWindow: unknown[];
331
- compactResponseId?: string;
332
- createdAt?: string;
333
- requestMeta?: NativeCompactionRequestMeta;
334
- };
335
-
336
- export type CreateNativeCompactionResultInput = {
337
- firstKeptEntryId: string;
338
- tokensBefore: number;
339
- details: NativeCompactionDetails;
340
- /**
341
- * Summary text extracted from the compact response. Stored as the entry summary so
342
- * pi's default replay still has real context after switching to an unsupported model.
343
- */
344
- summary?: string;
345
- };
346
-
347
- function isRecord(value: unknown): value is Record<string, unknown> {
348
- return !!value && typeof value === "object" && !Array.isArray(value);
349
- }
350
-
351
- function isNonEmptyString(value: unknown): value is string {
352
- return typeof value === "string" && value.trim().length > 0;
353
- }
354
-
355
- function isFiniteNonNegativeNumber(value: unknown): value is number {
356
- return typeof value === "number" && Number.isFinite(value) && value >= 0;
357
- }
358
-
359
- function normalizeString(value: string): string {
360
- return value.trim();
361
- }
362
-
363
- function isStructuredValue(value: unknown): boolean {
364
- if (
365
- value === null ||
366
- typeof value === "string" ||
367
- typeof value === "number" ||
368
- typeof value === "boolean"
369
- ) {
370
- return true;
371
- }
372
-
373
- if (Array.isArray(value)) {
374
- return value.every(isStructuredValue);
375
- }
376
-
377
- if (isRecord(value)) {
378
- return Object.values(value).every(isStructuredValue);
379
- }
380
-
381
- return false;
382
- }
383
-
384
- function cloneStructuredValue(value: unknown): unknown {
385
- if (
386
- value === null ||
387
- typeof value === "string" ||
388
- typeof value === "number" ||
389
- typeof value === "boolean"
390
- ) {
391
- return value;
392
- }
393
-
394
- if (Array.isArray(value)) {
395
- return value.map(cloneStructuredValue);
396
- }
397
-
398
- if (isRecord(value)) {
399
- const clone: Record<string, unknown> = {};
400
- for (const [key, nested] of Object.entries(value)) {
401
- clone[key] = cloneStructuredValue(nested);
402
- }
403
- return clone;
404
- }
405
-
406
- throw new Error(`Unsupported structured value: ${typeof value}`);
407
- }
408
-
409
- function isCompactedWindowItem(value: unknown): value is Record<string, unknown> {
410
- return isRecord(value) && Object.values(value).every(isStructuredValue);
411
- }
412
-
413
- export function isNativeCompactionRequestMeta(value: unknown): value is NativeCompactionRequestMeta {
414
- if (!isRecord(value)) {
415
- return false;
416
- }
417
-
418
- const { tokensBefore, previousSummaryPresent } = value;
419
- if (tokensBefore !== undefined && !isFiniteNonNegativeNumber(tokensBefore)) {
420
- return false;
421
- }
422
-
423
- if (previousSummaryPresent !== undefined && typeof previousSummaryPresent !== "boolean") {
424
- return false;
425
- }
426
-
427
- return true;
428
- }
429
-
430
- export function isNativeCompactionIdentity(value: unknown): value is NativeCompactionIdentity {
431
- if (!isRecord(value)) {
432
- return false;
433
- }
434
-
435
- return (
436
- isNonEmptyString(value.provider) &&
437
- isNonEmptyString(value.api) &&
438
- isNonEmptyString(value.model) &&
439
- isNonEmptyString(value.baseUrl)
440
- );
441
- }
442
-
443
- export function isNativeCompactionInputProvenance(value: unknown): value is NativeCompactionInputProvenance {
444
- return value === NATIVE_COMPACTION_INPUT_PROVENANCE || value === LEGACY_REMOTE_V2_INPUT_PROVENANCE;
445
- }
446
-
447
- export function getRemoteV2InputProvenance(source: RemoteV2ContextSource): NativeCompactionInputProvenance {
448
- return source === "pi-context-hook"
449
- ? NATIVE_COMPACTION_INPUT_PROVENANCE
450
- : LEGACY_REMOTE_V2_INPUT_PROVENANCE;
451
- }
452
-
453
- export function isDeferredToolCarryover(value: unknown): value is DeferredToolCarryoverV1 {
454
- if (!isRecord(value) || value.version !== 1 || value.source !== CACHE_STACK_ACTIVATION_ENTRY_TYPE) {
455
- return false;
456
- }
457
-
458
- return (
459
- Array.isArray(value.toolNames) &&
460
- value.toolNames.every((name) => isNonEmptyString(name)) &&
461
- (value.catalogHash === undefined || typeof value.catalogHash === "string")
462
- );
463
- }
464
-
465
- function normalizeDeferredToolCarryover(value: DeferredToolCarryoverV1): DeferredToolCarryoverV1 {
466
- if (!isDeferredToolCarryover(value)) {
467
- throw new Error("Invalid deferred tool carryover");
468
- }
469
-
470
- return {
471
- version: 1,
472
- source: CACHE_STACK_ACTIVATION_ENTRY_TYPE,
473
- toolNames: [...new Set(value.toolNames.map((name) => name.trim()).filter(Boolean))].sort(),
474
- ...(typeof value.catalogHash === "string" ? { catalogHash: value.catalogHash } : {}),
475
- };
476
- }
477
-
478
- function decodeCacheStackActivationState(value: unknown): DeferredToolCarryoverV1 | undefined {
479
- if (!isRecord(value) || value.version !== 1 || typeof value.catalogHash !== "string") {
480
- return undefined;
481
- }
482
- if (!Array.isArray(value.activatedTools) || !value.activatedTools.every((name) => typeof name === "string")) {
483
- return undefined;
484
- }
485
-
486
- return normalizeDeferredToolCarryover({
487
- version: 1,
488
- source: CACHE_STACK_ACTIVATION_ENTRY_TYPE,
489
- toolNames: value.activatedTools,
490
- catalogHash: value.catalogHash,
491
- });
492
- }
493
-
494
- /** Read only the latest valid cache-stack activation snapshot on this branch. */
495
- export function getLatestDeferredToolCarryover(
496
- entries: readonly SessionEntry[],
497
- ): DeferredToolCarryoverV1 | undefined {
498
- for (let index = entries.length - 1; index >= 0; index -= 1) {
499
- const entry = entries[index];
500
- if (entry?.type !== "custom" || entry.customType !== CACHE_STACK_ACTIVATION_ENTRY_TYPE) {
501
- continue;
502
- }
503
-
504
- const decoded = decodeCacheStackActivationState(entry.data);
505
- if (decoded) return decoded;
506
- }
507
-
508
- return undefined;
509
- }
510
-
511
- export function isNativeCompactionDetails(value: unknown): value is NativeCompactionDetails {
512
- if (!isRecord(value) || !isNativeCompactionIdentity(value)) {
513
- return false;
514
- }
515
-
516
- const candidate = value as Record<string, unknown>;
517
- return (
518
- (candidate.strategy === LEGACY_NATIVE_COMPACTION_STRATEGY ||
519
- candidate.strategy === REMOTE_V2_COMPACTION_STRATEGY) &&
520
- (candidate.inputProvenance === undefined || isNativeCompactionInputProvenance(candidate.inputProvenance)) &&
521
- (candidate.compactionModel === undefined || isNativeCompactionIdentity(candidate.compactionModel)) &&
522
- (candidate.deferredToolCarryover === undefined || isDeferredToolCarryover(candidate.deferredToolCarryover)) &&
523
- Array.isArray(candidate.compactedWindow) &&
524
- candidate.compactedWindow.every(isCompactedWindowItem) &&
525
- isNonEmptyString(candidate.createdAt) &&
526
- (candidate.compactResponseId === undefined || isNonEmptyString(candidate.compactResponseId)) &&
527
- (candidate.requestMeta === undefined || isNativeCompactionRequestMeta(candidate.requestMeta))
528
- );
529
- }
530
-
531
- export function isNativeCompactionEntry(value: unknown): value is NativeCompactionEntry {
532
- return isRecord(value) && value.type === "compaction" && isNativeCompactionDetails(value.details);
533
- }
534
-
535
- export function createNativeCompactionDetails(input: CreateNativeCompactionDetailsInput): NativeCompactionDetails {
536
- return {
537
- strategy: NATIVE_COMPACTION_STRATEGY,
538
- inputProvenance: input.inputProvenance,
539
- provider: normalizeString(input.provider),
540
- api: normalizeString(input.api),
541
- model: normalizeString(input.model),
542
- baseUrl: normalizeString(input.baseUrl),
543
- compactionModel: input.compactionModel
544
- ? {
545
- provider: normalizeString(input.compactionModel.provider),
546
- api: normalizeString(input.compactionModel.api),
547
- model: normalizeString(input.compactionModel.model),
548
- baseUrl: normalizeString(input.compactionModel.baseUrl),
549
- }
550
- : undefined,
551
- deferredToolCarryover: input.deferredToolCarryover
552
- ? normalizeDeferredToolCarryover(input.deferredToolCarryover)
553
- : undefined,
554
- compactedWindow: input.compactedWindow.map((item) => cloneStructuredValue(item)),
555
- compactResponseId: isNonEmptyString(input.compactResponseId) ? normalizeString(input.compactResponseId) : undefined,
556
- createdAt: isNonEmptyString(input.createdAt) ? normalizeString(input.createdAt) : new Date().toISOString(),
557
- requestMeta: input.requestMeta
558
- ? {
559
- ...(input.requestMeta.tokensBefore !== undefined ? { tokensBefore: input.requestMeta.tokensBefore } : {}),
560
- ...(input.requestMeta.previousSummaryPresent !== undefined
561
- ? { previousSummaryPresent: input.requestMeta.previousSummaryPresent }
562
- : {}),
563
- }
564
- : undefined,
565
- };
566
- }
567
-
568
- export function createNativeCompactionResult(
569
- input: CreateNativeCompactionResultInput,
570
- ): CompactionResult<NativeCompactionDetails> {
571
- const summary = input.summary?.trim();
572
- return {
573
- summary: summary && summary.length > 0 ? summary : NATIVE_COMPACTION_FALLBACK_SUMMARY,
574
- firstKeptEntryId: input.firstKeptEntryId,
575
- tokensBefore: input.tokensBefore,
576
- details: input.details,
577
- };
578
- }
579
-
580
- export const DEFAULT_NATIVE_FALLBACK_CONFIG: NativeFallbackConfig = {
581
- enabled: true,
582
- model: undefined,
583
- thinkingLevel: "off",
584
- };
585
-
586
- export const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {
587
- enabled: true,
588
- contextManagement: "off",
589
- allowCompactionContinuityBreak: false,
590
- remoteCompactModel: undefined,
591
- remoteV2ContextSource: "legacy",
592
-
593
- nativeFallback: { ...DEFAULT_NATIVE_FALLBACK_CONFIG },
594
- responsesApis: [...RESPONSES_COMPACT_CAPABLE_APIS],
595
- gatewayContextModels: [],
596
- leaveManagedMode: "warn",
597
- contextReminderThresholdPercent: 5,
598
- notifyOnLoad: false,
599
- debug: false,
600
- logProviderPayloads: false,
601
- logCompactResponses: false,
602
- redactSensitiveData: true,
603
- artifactRoot: DEFAULT_ARTIFACT_ROOT,
604
- };
605
-
606
- export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchConfig = {
607
- enabled: true,
608
- models: [],
609
- };
610
-
611
- /** Single source of truth for the hosted image model used when nothing is configured. */
612
- export const DEFAULT_IMAGE_GENERATION_MODEL = "grok-imagine-image-2.0";
613
- export const DEFAULT_IMAGE_GENERATION_MODELS: readonly string[] = [DEFAULT_IMAGE_GENERATION_MODEL];
614
-
615
- export const DEFAULT_IMAGE_GENERATION_CONFIG: ImageGenerationConfig = {
616
- enabled: false,
617
- models: [...DEFAULT_IMAGE_GENERATION_MODELS],
618
- };
619
-
620
- export const DEFAULT_AUTO_MODE_CONFIG: AutoModeConfig = {
621
- enabled: true,
622
- models: [],
623
- reviewerModel: undefined,
624
- gate: "side-effect",
625
- extraTools: [],
626
- timeoutMs: 30_000,
627
- transcript: true,
628
- evidenceTools: true,
629
- maxEvidenceRounds: 3,
630
- classifier: {
631
- enabled: false,
632
- model: undefined,
633
- timeoutMs: DEFAULT_CLASSIFIER_TIMEOUT_MS,
634
- maxLag: 2,
635
- },
636
- circuitBreaker: {
637
- consecutiveDenials: 3,
638
- recentDenials: 10,
639
- windowSize: 50,
640
- },
641
- };
642
-
643
- export const DEFAULT_TOOLKIT_CONFIG: ToolkitConfig = {
644
- compaction: DEFAULT_COMPACTION_CONFIG,
645
- webSearch: DEFAULT_WEB_SEARCH_CONFIG,
646
- imageGeneration: DEFAULT_IMAGE_GENERATION_CONFIG,
647
- autoMode: DEFAULT_AUTO_MODE_CONFIG,
648
- };
1
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
+ import type { ConfigDocumentSnapshot } from "./config/policy";
3
+ import type { CompactionEntry, CompactionResult, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
4
+
5
+ export const TOOLKIT_ID = "pi-openai-toolkit";
6
+ export const COMPACTION_EXTENSION_ID = `${TOOLKIT_ID}:compaction`;
7
+ export const WEB_SEARCH_EXTENSION_ID = `${TOOLKIT_ID}:web-search`;
8
+ export const IMAGE_GENERATION_EXTENSION_ID = `${TOOLKIT_ID}:image-generation`;
9
+ export const DEFAULT_ARTIFACT_ROOT = "~/.pi/agent/artifacts/pi-openai-toolkit/compaction";
10
+ export const REDACTED_VALUE = "[REDACTED]";
11
+ /**
12
+ * APIs the extension knows how to build a `/responses/compact` URL for.
13
+ * `compaction.responsesApis` in config.json may only narrow this set.
14
+ */
15
+ export const RESPONSES_COMPACT_CAPABLE_APIS = [
16
+ "openai-responses",
17
+ "openai-codex-responses",
18
+ "azure-openai-responses",
19
+ ] as const;
20
+ /**
21
+ * Bare model ID used by the independent Astra effort compatibility layer.
22
+ * Remote Context eligibility is API/provider based with an exact gateway opt-in;
23
+ * this constant does not verify backend support or grant that opt-in.
24
+ */
25
+ export const ASTRA_MODEL_ID = "gpt-6-astra";
26
+ export const LEGACY_NATIVE_COMPACTION_STRATEGY = "openai-native-compact-v1";
27
+ export const REMOTE_V2_COMPACTION_STRATEGY = "openai-remote-compaction-v2";
28
+ export const NATIVE_COMPACTION_STRATEGY = REMOTE_V2_COMPACTION_STRATEGY;
29
+ /**
30
+ * Provenance recorded when the checkpoint input came from Pi's ordered context
31
+ * hook projection. Older persisted entries may omit it and must not be
32
+ * replayed by Remote V2.
33
+ */
34
+ export const NATIVE_COMPACTION_INPUT_PROVENANCE = "pi-context-hook-v1" as const;
35
+ /** Explicit opt-in marker for the pre-projection raw session/branch chain. */
36
+ export const LEGACY_REMOTE_V2_INPUT_PROVENANCE = "legacy-raw-context-v1" as const;
37
+ export type NativeCompactionInputProvenance =
38
+ | typeof NATIVE_COMPACTION_INPUT_PROVENANCE
39
+ | typeof LEGACY_REMOTE_V2_INPUT_PROVENANCE;
40
+ /**
41
+ * Pi currently requires CompactionResult.summary to be text. This marker is only a
42
+ * replay shim; the provider receives the opaque item from details.compactedWindow.
43
+ */
44
+ export const NATIVE_COMPACTION_FALLBACK_SUMMARY = "[OpenAI remote v2 opaque compaction checkpoint]";
45
+ export const STANDALONE_COMPACTION_FALLBACK_SUMMARY = "[OpenAI standalone opaque compaction window]";
46
+
47
+ export const THINKING_LEVELS: readonly ThinkingLevel[] = [
48
+ "off",
49
+ "minimal",
50
+ "low",
51
+ "medium",
52
+ "high",
53
+ "xhigh",
54
+ "max",
55
+ ];
56
+
57
+ export type DebugArtifactKind =
58
+ | "provider-request"
59
+ | "compact-response"
60
+ | "compaction-event"
61
+ | "lifecycle";
62
+
63
+ export type ContextManagementMode = "off" | "remote";
64
+
65
+ /** How Remote V2 obtains the history it sends to the compaction endpoint. */
66
+ export type RemoteV2ContextSource = "pi-context-hook" | "legacy";
67
+
68
+ /** Native-method fallback compaction: which model runs pi's compact() and how deeply. */
69
+ export type NativeFallbackConfig = {
70
+ enabled: boolean;
71
+ /**
72
+ * "provider/model-id" used for native-method fallback compaction when the active model
73
+ * cannot use remote v2 at all. A failed remote request is instead summarized by
74
+ * `remoteCompactModel`; the two sources never substitute for each other. Unset means the
75
+ * active model via pi's default path.
76
+ */
77
+ model?: string;
78
+ /** Thinking level passed to pi's native compact() when the fallback model runs. */
79
+ thinkingLevel: ThinkingLevel;
80
+ };
81
+
82
+ /**
83
+ * Legacy compatibility setting. Remote Context history is no longer bulk-compacted when
84
+ * selecting a non-window model; local projection and a no-summary boundary own that handoff.
85
+ */
86
+ export type LeaveManagedModePolicy = "warn" | "compact";
87
+
88
+ export type DebugConfig = {
89
+ debug: boolean;
90
+ notifyOnLoad: boolean;
91
+ logProviderPayloads: boolean;
92
+ logCompactResponses: boolean;
93
+ redactSensitiveData: boolean;
94
+ artifactRoot: string;
95
+ };
96
+
97
+ export type CompactionConfig = {
98
+ enabled: boolean;
99
+ /**
100
+ * Legacy compatibility setting. Non-window consumers never receive retired Remote Context
101
+ * history, regardless of this value; retained config files continue to parse it.
102
+ */
103
+ leaveManagedMode: LeaveManagedModePolicy;
104
+ /** Optional Codex Remote Context management. Disabled by default for compatibility. */
105
+ contextManagement: ContextManagementMode;
106
+ /**
107
+ * Allow a Responses session whose latest compaction was not created by this extension
108
+ * to restart native compaction from Pi's current serialized session context.
109
+ */
110
+ allowCompactionContinuityBreak: boolean;
111
+ /**
112
+ * "provider/model-id" used only for remote_compaction_v2. The active session model
113
+ * remains the checkpoint consumer and continues handling normal requests.
114
+ */
115
+ remoteCompactModel?: string;
116
+ /**
117
+ * Remote V2 input source. Pi's patched context hook preserves the normal
118
+ * provider-visible projection; legacy keeps the pre-patch raw session/branch chain.
119
+ */
120
+ remoteV2ContextSource: RemoteV2ContextSource;
121
+ /** Native-method fallback compaction policy (non-Responses APIs, or when the compact endpoint fails). */
122
+ nativeFallback: NativeFallbackConfig;
123
+ /**
124
+ * Subset of RESPONSES_COMPACT_CAPABLE_APIS that should use remote compaction.
125
+ */
126
+ responsesApis: string[];
127
+ /**
128
+ * Exact "provider/model" keys allowed to use Codex Remote Context on the
129
+ * `openai-responses` gateway wire. The native `openai-codex` route ignores
130
+ * this list; gateway coverage is opt-in per model.
131
+ */
132
+ gatewayContextModels: string[];
133
+ /**
134
+ * Percentage of the context window that, when remaining tokens drop below it,
135
+ * triggers a checkpoint/new_context reminder (0-100; 0 disables reminders).
136
+ */
137
+ contextReminderThresholdPercent: number;
138
+ notifyOnLoad: boolean;
139
+ debug: boolean;
140
+ logProviderPayloads: boolean;
141
+ logCompactResponses: boolean;
142
+ redactSensitiveData: boolean;
143
+ artifactRoot: string;
144
+ };
145
+
146
+ export type WebSearchRoute = "local" | "hosted" | "standalone-alpha";
147
+
148
+ export type WebSearchConfig = {
149
+ enabled: boolean;
150
+ /** Exact provider/model keys allowed to use toolkit-native Web Search. */
151
+ models: string[];
152
+ /** Explicit route for every model not matched by `routes`. */
153
+ defaultRoute?: WebSearchRoute;
154
+ /** Exact `provider/model-id` overrides; no globs or fuzzy matching. */
155
+ routes?: Record<string, WebSearchRoute>;
156
+ };
157
+
158
+ export type ImageGenerationConfig = {
159
+ enabled: boolean;
160
+ /**
161
+ * Bare Images API model ids this toolkit may send, without a `provider/` prefix.
162
+ * The first legacy entry is the default; `openai_generate_image`'s optional `model` argument
163
+ * may select any other entry in this order-preserving list.
164
+ */
165
+ models: string[];
166
+ };
167
+
168
+ export type ToolkitConfig = {
169
+ compaction: CompactionConfig;
170
+ webSearch: WebSearchConfig;
171
+ imageGeneration: ImageGenerationConfig;
172
+ };
173
+
174
+ export type LoadedToolkitConfig = {
175
+ /** Legacy normalization seam. Feature entrypoints must resolve this snapshot for their model. */
176
+ config: ToolkitConfig;
177
+ /** Optional only for existing injected legacy readers; real loads always include it. */
178
+ document?: ConfigDocumentSnapshot;
179
+ /** Path of the config file that was applied, if it existed and parsed. */
180
+ source?: string;
181
+ warnings: string[];
182
+ };
183
+
184
+ export type ArtifactPaths = {
185
+ rootDir: string;
186
+ sessionDir: string;
187
+ providerRequestsDir: string;
188
+ compactResponsesDir: string;
189
+ compactionDir: string;
190
+ lifecycleDir: string;
191
+ };
192
+
193
+ export type ArtifactSessionInfo = {
194
+ cwd: string;
195
+ sessionId?: string;
196
+ sessionFile?: string;
197
+ sessionDir?: string;
198
+ };
199
+
200
+ export type ArtifactContext = ArtifactSessionInfo | Pick<ExtensionContext, "cwd" | "sessionManager">;
201
+
202
+ export type DebugArtifactEnvelope = {
203
+ extension: string;
204
+ kind: DebugArtifactKind;
205
+ timestamp: string;
206
+ cwd: string;
207
+ sessionId?: string;
208
+ sessionFile?: string;
209
+ sessionDir?: string;
210
+ redaction: {
211
+ enabled: boolean;
212
+ };
213
+ data: unknown;
214
+ };
215
+
216
+ export type RedactOptions = {
217
+ placeholder?: string;
218
+ };
219
+
220
+ export type NativeCompactionStrategy =
221
+ | typeof LEGACY_NATIVE_COMPACTION_STRATEGY
222
+ | typeof REMOTE_V2_COMPACTION_STRATEGY;
223
+
224
+ export type NativeCompactionRequestMeta = {
225
+ tokensBefore?: number;
226
+ previousSummaryPresent?: boolean;
227
+ };
228
+
229
+ export type NativeCompactionIdentity = {
230
+ provider: string;
231
+ api: string;
232
+ model: string;
233
+ baseUrl: string;
234
+ };
235
+
236
+ export const CACHE_STACK_ACTIVATION_ENTRY_TYPE = "pi-cache-stack.activation-state.v1" as const;
237
+
238
+ export type DeferredToolCarryoverV1 = {
239
+ version: 1;
240
+ source: typeof CACHE_STACK_ACTIVATION_ENTRY_TYPE;
241
+ toolNames: string[];
242
+ catalogHash?: string;
243
+ };
244
+
245
+ export type NativeCompactionDetails = NativeCompactionIdentity & {
246
+ strategy: NativeCompactionStrategy;
247
+ /**
248
+ * Input source recorded for checkpoints created by this extension. Older
249
+ * persisted entries may omit it and must not be treated as verified for
250
+ * Remote V2 replay.
251
+ */
252
+ inputProvenance?: NativeCompactionInputProvenance;
253
+ /** Actual producer of the opaque checkpoint; absent on legacy same-model entries. */
254
+ compactionModel?: NativeCompactionIdentity;
255
+ /** Cache-stack activation state captured at this opaque checkpoint. */
256
+ deferredToolCarryover?: DeferredToolCarryoverV1;
257
+ compactedWindow: unknown[];
258
+ compactResponseId?: string;
259
+ createdAt: string;
260
+ requestMeta?: NativeCompactionRequestMeta;
261
+ };
262
+
263
+ export type NativeCompactionEntry = CompactionEntry<NativeCompactionDetails>;
264
+
265
+ export type CreateNativeCompactionDetailsInput = NativeCompactionIdentity & {
266
+ strategy?: NativeCompactionStrategy;
267
+ inputProvenance: NativeCompactionInputProvenance;
268
+ compactionModel?: NativeCompactionIdentity;
269
+ deferredToolCarryover?: DeferredToolCarryoverV1;
270
+ compactedWindow: unknown[];
271
+ compactResponseId?: string;
272
+ createdAt?: string;
273
+ requestMeta?: NativeCompactionRequestMeta;
274
+ };
275
+
276
+ export type CreateNativeCompactionResultInput = {
277
+ firstKeptEntryId: string;
278
+ tokensBefore: number;
279
+ details: NativeCompactionDetails;
280
+ /**
281
+ * An explicitly supplied genuine summary. Retained standalone assistant text is
282
+ * not a summary of the sealed history; ordinary remote success uses an opaque marker.
283
+ */
284
+ summary?: string;
285
+ };
286
+
287
+ function isRecord(value: unknown): value is Record<string, unknown> {
288
+ return !!value && typeof value === "object" && !Array.isArray(value);
289
+ }
290
+
291
+ function isNonEmptyString(value: unknown): value is string {
292
+ return typeof value === "string" && value.trim().length > 0;
293
+ }
294
+
295
+ function isFiniteNonNegativeNumber(value: unknown): value is number {
296
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
297
+ }
298
+
299
+ function normalizeString(value: string): string {
300
+ return value.trim();
301
+ }
302
+
303
+ function isStructuredValue(value: unknown): boolean {
304
+ if (
305
+ value === null ||
306
+ typeof value === "string" ||
307
+ (typeof value === "number" && Number.isFinite(value)) ||
308
+ typeof value === "boolean"
309
+ ) {
310
+ return true;
311
+ }
312
+
313
+ if (Array.isArray(value)) {
314
+ return value.every(isStructuredValue);
315
+ }
316
+
317
+ if (isRecord(value)) {
318
+ return Object.values(value).every(isStructuredValue);
319
+ }
320
+
321
+ return false;
322
+ }
323
+
324
+ function cloneStructuredValue(value: unknown): unknown {
325
+ if (
326
+ value === null ||
327
+ typeof value === "string" ||
328
+ typeof value === "number" ||
329
+ typeof value === "boolean"
330
+ ) {
331
+ return value;
332
+ }
333
+
334
+ if (Array.isArray(value)) {
335
+ return value.map(cloneStructuredValue);
336
+ }
337
+
338
+ if (isRecord(value)) {
339
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, cloneStructuredValue(nested)]));
340
+ }
341
+
342
+ throw new Error(`Unsupported structured value: ${typeof value}`);
343
+ }
344
+
345
+ export function isCompactedWindowItem(value: unknown): value is Record<string, unknown> {
346
+ return isRecord(value) && Object.values(value).every(isStructuredValue);
347
+ }
348
+
349
+ export function isNativeCompactionRequestMeta(value: unknown): value is NativeCompactionRequestMeta {
350
+ if (!isRecord(value)) {
351
+ return false;
352
+ }
353
+
354
+ const { tokensBefore, previousSummaryPresent } = value;
355
+ if (tokensBefore !== undefined && !isFiniteNonNegativeNumber(tokensBefore)) {
356
+ return false;
357
+ }
358
+
359
+ if (previousSummaryPresent !== undefined && typeof previousSummaryPresent !== "boolean") {
360
+ return false;
361
+ }
362
+
363
+ return true;
364
+ }
365
+
366
+ export function isNativeCompactionIdentity(value: unknown): value is NativeCompactionIdentity {
367
+ if (!isRecord(value)) {
368
+ return false;
369
+ }
370
+
371
+ return (
372
+ isNonEmptyString(value.provider) &&
373
+ isNonEmptyString(value.api) &&
374
+ isNonEmptyString(value.model) &&
375
+ isNonEmptyString(value.baseUrl)
376
+ );
377
+ }
378
+
379
+ export function isNativeCompactionInputProvenance(value: unknown): value is NativeCompactionInputProvenance {
380
+ return value === NATIVE_COMPACTION_INPUT_PROVENANCE || value === LEGACY_REMOTE_V2_INPUT_PROVENANCE;
381
+ }
382
+
383
+ export function getRemoteV2InputProvenance(source: RemoteV2ContextSource): NativeCompactionInputProvenance {
384
+ return source === "pi-context-hook"
385
+ ? NATIVE_COMPACTION_INPUT_PROVENANCE
386
+ : LEGACY_REMOTE_V2_INPUT_PROVENANCE;
387
+ }
388
+
389
+ export function isDeferredToolCarryover(value: unknown): value is DeferredToolCarryoverV1 {
390
+ if (!isRecord(value) || value.version !== 1 || value.source !== CACHE_STACK_ACTIVATION_ENTRY_TYPE) {
391
+ return false;
392
+ }
393
+
394
+ return (
395
+ Array.isArray(value.toolNames) &&
396
+ value.toolNames.every((name) => isNonEmptyString(name)) &&
397
+ (value.catalogHash === undefined || typeof value.catalogHash === "string")
398
+ );
399
+ }
400
+
401
+ function normalizeDeferredToolCarryover(value: DeferredToolCarryoverV1): DeferredToolCarryoverV1 {
402
+ if (!isDeferredToolCarryover(value)) {
403
+ throw new Error("Invalid deferred tool carryover");
404
+ }
405
+
406
+ return {
407
+ version: 1,
408
+ source: CACHE_STACK_ACTIVATION_ENTRY_TYPE,
409
+ toolNames: [...new Set(value.toolNames.map((name) => name.trim()).filter(Boolean))].sort(),
410
+ ...(typeof value.catalogHash === "string" ? { catalogHash: value.catalogHash } : {}),
411
+ };
412
+ }
413
+
414
+ function decodeCacheStackActivationState(value: unknown): DeferredToolCarryoverV1 | undefined {
415
+ if (!isRecord(value) || value.version !== 1 || typeof value.catalogHash !== "string") {
416
+ return undefined;
417
+ }
418
+ if (!Array.isArray(value.activatedTools) || !value.activatedTools.every((name) => typeof name === "string")) {
419
+ return undefined;
420
+ }
421
+
422
+ return normalizeDeferredToolCarryover({
423
+ version: 1,
424
+ source: CACHE_STACK_ACTIVATION_ENTRY_TYPE,
425
+ toolNames: value.activatedTools,
426
+ catalogHash: value.catalogHash,
427
+ });
428
+ }
429
+
430
+ /** Read only the latest valid cache-stack activation snapshot on this branch. */
431
+ export function getLatestDeferredToolCarryover(
432
+ entries: readonly SessionEntry[],
433
+ ): DeferredToolCarryoverV1 | undefined {
434
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
435
+ const entry = entries[index];
436
+ if (entry?.type !== "custom" || entry.customType !== CACHE_STACK_ACTIVATION_ENTRY_TYPE) {
437
+ continue;
438
+ }
439
+
440
+ const decoded = decodeCacheStackActivationState(entry.data);
441
+ if (decoded) return decoded;
442
+ }
443
+
444
+ return undefined;
445
+ }
446
+
447
+ export function isNativeCompactionDetails(value: unknown): value is NativeCompactionDetails {
448
+ if (!isRecord(value) || !isNativeCompactionIdentity(value)) {
449
+ return false;
450
+ }
451
+
452
+ const candidate = value as Record<string, unknown>;
453
+ return (
454
+ (candidate.strategy === LEGACY_NATIVE_COMPACTION_STRATEGY ||
455
+ candidate.strategy === REMOTE_V2_COMPACTION_STRATEGY) &&
456
+ (candidate.inputProvenance === undefined || isNativeCompactionInputProvenance(candidate.inputProvenance)) &&
457
+ (candidate.compactionModel === undefined || isNativeCompactionIdentity(candidate.compactionModel)) &&
458
+ (candidate.deferredToolCarryover === undefined || isDeferredToolCarryover(candidate.deferredToolCarryover)) &&
459
+ Array.isArray(candidate.compactedWindow) &&
460
+ candidate.compactedWindow.every(isCompactedWindowItem) &&
461
+ isNonEmptyString(candidate.createdAt) &&
462
+ (candidate.compactResponseId === undefined || isNonEmptyString(candidate.compactResponseId)) &&
463
+ (candidate.requestMeta === undefined || isNativeCompactionRequestMeta(candidate.requestMeta))
464
+ );
465
+ }
466
+
467
+ export function isNativeCompactionEntry(value: unknown): value is NativeCompactionEntry {
468
+ return isRecord(value) && value.type === "compaction" && isNativeCompactionDetails(value.details);
469
+ }
470
+
471
+ export function createNativeCompactionDetails(input: CreateNativeCompactionDetailsInput): NativeCompactionDetails {
472
+ if (input.strategy !== undefined && input.strategy !== LEGACY_NATIVE_COMPACTION_STRATEGY && input.strategy !== REMOTE_V2_COMPACTION_STRATEGY) {
473
+ throw new Error("Invalid native compaction strategy");
474
+ }
475
+ return {
476
+ strategy: input.strategy ?? NATIVE_COMPACTION_STRATEGY,
477
+ inputProvenance: input.inputProvenance,
478
+ provider: normalizeString(input.provider),
479
+ api: normalizeString(input.api),
480
+ model: normalizeString(input.model),
481
+ baseUrl: normalizeString(input.baseUrl),
482
+ compactionModel: input.compactionModel
483
+ ? {
484
+ provider: normalizeString(input.compactionModel.provider),
485
+ api: normalizeString(input.compactionModel.api),
486
+ model: normalizeString(input.compactionModel.model),
487
+ baseUrl: normalizeString(input.compactionModel.baseUrl),
488
+ }
489
+ : undefined,
490
+ deferredToolCarryover: input.deferredToolCarryover
491
+ ? normalizeDeferredToolCarryover(input.deferredToolCarryover)
492
+ : undefined,
493
+ compactedWindow: input.compactedWindow.map((item) => cloneStructuredValue(item)),
494
+ compactResponseId: isNonEmptyString(input.compactResponseId) ? normalizeString(input.compactResponseId) : undefined,
495
+ createdAt: isNonEmptyString(input.createdAt) ? normalizeString(input.createdAt) : new Date().toISOString(),
496
+ requestMeta: input.requestMeta
497
+ ? {
498
+ ...(input.requestMeta.tokensBefore !== undefined ? { tokensBefore: input.requestMeta.tokensBefore } : {}),
499
+ ...(input.requestMeta.previousSummaryPresent !== undefined
500
+ ? { previousSummaryPresent: input.requestMeta.previousSummaryPresent }
501
+ : {}),
502
+ }
503
+ : undefined,
504
+ };
505
+ }
506
+
507
+ export function createNativeCompactionResult(
508
+ input: CreateNativeCompactionResultInput,
509
+ ): CompactionResult<NativeCompactionDetails> {
510
+ const summary = input.summary?.trim();
511
+ return {
512
+ summary: summary && summary.length > 0 ? summary
513
+ : input.details.strategy === LEGACY_NATIVE_COMPACTION_STRATEGY ? STANDALONE_COMPACTION_FALLBACK_SUMMARY
514
+ : NATIVE_COMPACTION_FALLBACK_SUMMARY,
515
+ firstKeptEntryId: input.firstKeptEntryId,
516
+ tokensBefore: input.tokensBefore,
517
+ details: input.details,
518
+ };
519
+ }
520
+
521
+ export const DEFAULT_NATIVE_FALLBACK_CONFIG: NativeFallbackConfig = {
522
+ enabled: true,
523
+ model: undefined,
524
+ thinkingLevel: "off",
525
+ };
526
+
527
+ export const DEFAULT_COMPACTION_CONFIG: CompactionConfig = {
528
+ enabled: true,
529
+ contextManagement: "off",
530
+ allowCompactionContinuityBreak: false,
531
+ remoteCompactModel: undefined,
532
+ remoteV2ContextSource: "legacy",
533
+
534
+ nativeFallback: { ...DEFAULT_NATIVE_FALLBACK_CONFIG },
535
+ responsesApis: [...RESPONSES_COMPACT_CAPABLE_APIS],
536
+ gatewayContextModels: [],
537
+ leaveManagedMode: "warn",
538
+ contextReminderThresholdPercent: 5,
539
+ notifyOnLoad: false,
540
+ debug: false,
541
+ logProviderPayloads: false,
542
+ logCompactResponses: false,
543
+ redactSensitiveData: true,
544
+ artifactRoot: DEFAULT_ARTIFACT_ROOT,
545
+ };
546
+
547
+ export const DEFAULT_WEB_SEARCH_CONFIG: WebSearchConfig = {
548
+ enabled: true,
549
+ models: [],
550
+ };
551
+
552
+ /** Single source of truth for the hosted image model used when nothing is configured. */
553
+ export const DEFAULT_IMAGE_GENERATION_MODEL = "grok-imagine-image-2.0";
554
+ export const DEFAULT_IMAGE_GENERATION_MODELS: readonly string[] = [DEFAULT_IMAGE_GENERATION_MODEL];
555
+
556
+ export const DEFAULT_IMAGE_GENERATION_CONFIG: ImageGenerationConfig = {
557
+ enabled: false,
558
+ models: [...DEFAULT_IMAGE_GENERATION_MODELS],
559
+ };
560
+
561
+ export const DEFAULT_TOOLKIT_CONFIG: ToolkitConfig = {
562
+ compaction: DEFAULT_COMPACTION_CONFIG,
563
+ webSearch: DEFAULT_WEB_SEARCH_CONFIG,
564
+ imageGeneration: DEFAULT_IMAGE_GENERATION_CONFIG,
565
+ };