pi-openai-toolkit 0.14.11 → 0.14.13

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