pi-openai-toolkit 0.20.8 → 0.21.1

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.
@@ -199,10 +199,69 @@ function buildCacheKey(model: Model<Api>, options: StreamOptions, requestHeaders
199
199
  return [model.provider, model.id, cleanBaseUrl(model.baseUrl), options.sessionId, stableHash(JSON.stringify(identity))].join("\u0000");
200
200
  }
201
201
 
202
+ // Only transport failures (not arbitrary caught errors) authorize auto fallback.
203
+ class RecoverableWebSocketError extends Error {}
204
+
205
+ const STREAM_INTERRUPTION = /\bstream closed before response\.completed\b/iu;
206
+ const TERMINAL_FAILURE_TEXT = /quota|billing|budget|usage.?limit|api.?key|unauthori[sz]ed|forbidden|authenticat|permission|invalid|malformed|unsupported|policy|context.?length|context.?overflow|\babort(?:ed)?\b|\bcancell?ed\b/iu;
207
+ const TRANSIENT_ERROR_CODES = new Set(["stream_error", "server_error", "request_timeout"]);
208
+
209
+ function transportError(error: unknown): Error {
210
+ const message = error instanceof Error ? error.message : String(error);
211
+ if ((error instanceof Error && error.name === "AbortError") || TERMINAL_FAILURE_TEXT.test(message)) {
212
+ return error instanceof Error ? error : new Error(message);
213
+ }
214
+ return new RecoverableWebSocketError(/^WebSocket (?:error|closed|.*timeout)/iu.test(message)
215
+ ? message : `WebSocket error: ${message}`);
216
+ }
217
+
202
218
  function socketError(event: WebSocketEvent): Error {
203
- if (typeof event.message === "string" && event.message.length > 0) return new Error(event.message);
204
- if (event.error instanceof Error) return event.error;
205
- return new Error("WebSocket transport failed");
219
+ if (typeof event.message === "string" && event.message.length > 0) return transportError(event.message);
220
+ if (event.error instanceof Error) return transportError(event.error);
221
+ return transportError("WebSocket transport failed");
222
+ }
223
+
224
+ function socketCloseError(event: WebSocketEvent, streaming: boolean): Error {
225
+ const suffix = event.reason ? `: ${event.reason}` : "";
226
+ if ([1002, 1003, 1007, 1008].includes(event.code ?? 0) || TERMINAL_FAILURE_TEXT.test(event.reason ?? "")) {
227
+ return new Error(`Responses connection rejected${event.code ? ` (${event.code})` : ""}${suffix}`);
228
+ }
229
+ return transportError(streaming
230
+ ? `WebSocket closed before response completion${suffix}`
231
+ : `WebSocket closed${event.code ? ` ${event.code}` : ""}${suffix}`);
232
+ }
233
+
234
+ function compatibleFailureMetadata(error: Record<string, unknown>): boolean {
235
+ if (typeof error.message === "string" && TERMINAL_FAILURE_TEXT.test(error.message)) return false;
236
+ for (const field of ["code", "type"]) {
237
+ const value = error[field];
238
+ if (value !== undefined && value !== null && value !== "" &&
239
+ (typeof value !== "string" || !TRANSIENT_ERROR_CODES.has(value.toLowerCase()))) return false;
240
+ }
241
+ for (const field of ["status", "status_code"]) {
242
+ const value = error[field];
243
+ if (value !== undefined && value !== null &&
244
+ (typeof value !== "number" || ![408, 429, 500, 502, 503, 504].includes(value))) return false;
245
+ }
246
+ return true;
247
+ }
248
+
249
+ function normalizeStreamInterruption(event: Record<string, unknown>): Record<string, unknown> | undefined {
250
+ const response = isRecord(event.response) ? event.response : undefined;
251
+ const error = event.type === "error" ? event : event.type === "response.failed" && isRecord(response?.error) ? response.error : undefined;
252
+ if (!error || typeof error.message !== "string" || !STREAM_INTERRUPTION.test(error.message) ||
253
+ TERMINAL_FAILURE_TEXT.test(error.message)) return undefined;
254
+ // Envelope type/status describe the event/response, not its failure category.
255
+ // Still check their code/HTTP status so a nested transient message cannot
256
+ // override explicit fatal metadata on an enclosing envelope.
257
+ const metadata: Record<string, unknown>[] = [{ ...event, type: undefined }];
258
+ if (event.type === "response.failed") {
259
+ metadata.push({ ...response, type: undefined, status: response?.status === "failed" ? undefined : response?.status }, error);
260
+ }
261
+ if (isRecord(error.error)) metadata.push(error.error);
262
+ if (metadata.some((value) => !compatibleFailureMetadata(value))) return undefined;
263
+ const normalized = { ...error, message: `WebSocket error: ${error.message}` };
264
+ return event.type === "error" ? normalized : { ...event, response: { ...response, error: normalized } };
206
265
  }
207
266
 
208
267
  function closeSocket(socket: WebSocketLike | undefined, reason = "done"): void {
@@ -247,8 +306,9 @@ export function closeResponsesWebSocketSessions(sessionId?: string): void {
247
306
  }
248
307
 
249
308
 
250
- function liveSignals(options: StreamOptions, init?: RequestInit): AbortSignal[] {
251
- return [options.signal, init?.signal].filter((signal): signal is AbortSignal => signal !== undefined);
309
+ function liveSignals(options: StreamOptions, input: RequestInfo | URL, init?: RequestInit): AbortSignal[] {
310
+ return [...new Set([options.signal, init?.signal, input instanceof Request ? input.signal : undefined]
311
+ .filter((signal): signal is AbortSignal => signal !== undefined && signal !== null))];
252
312
  }
253
313
 
254
314
  async function connectWebSocket(
@@ -258,7 +318,7 @@ async function connectWebSocket(
258
318
  timeoutMs: number,
259
319
  ): Promise<WebSocketLike> {
260
320
  const WebSocketCtor = globalThis.WebSocket as unknown as WebSocketConstructor | undefined;
261
- if (typeof WebSocketCtor !== "function") throw new Error("WebSocket transport is not available in this runtime");
321
+ if (typeof WebSocketCtor !== "function") throw transportError("WebSocket transport is not available in this runtime");
262
322
  if (signals.some((signal) => signal.aborted)) throw new Error("Request was aborted");
263
323
 
264
324
  return new Promise((resolve, reject) => {
@@ -286,9 +346,7 @@ async function connectWebSocket(
286
346
  resolve(socket!);
287
347
  };
288
348
  const onError = (event: WebSocketEvent) => fail(socketError(event));
289
- const onClose = (event: WebSocketEvent) => fail(new Error(
290
- `WebSocket closed${typeof event.code === "number" ? ` ${event.code}` : ""}${event.reason ? ` ${event.reason}` : ""}`,
291
- ));
349
+ const onClose = (event: WebSocketEvent) => fail(socketCloseError(event, false));
292
350
  const onAbort = () => fail(new Error("Request was aborted"));
293
351
 
294
352
  try {
@@ -301,7 +359,8 @@ async function connectWebSocket(
301
359
  socket.addEventListener("error", onError);
302
360
  socket.addEventListener("close", onClose);
303
361
  for (const signal of signals) signal.addEventListener("abort", onAbort, { once: true });
304
- if (timeoutMs > 0) timeout = setTimeout(() => fail(new Error(`WebSocket connect timeout after ${timeoutMs}ms`)), timeoutMs);
362
+ if (signals.some((signal) => signal.aborted)) { onAbort(); return; }
363
+ if (timeoutMs > 0) timeout = setTimeout(() => fail(transportError(`WebSocket connect timeout after ${timeoutMs}ms`)), timeoutMs);
305
364
  });
306
365
  }
307
366
 
@@ -439,22 +498,26 @@ function completedContinuation(entry: CachedSocketEntry | undefined, fullPayload
439
498
  return true;
440
499
  }
441
500
 
442
- function createSseResponse(
501
+ async function createSseResponse(
443
502
  socket: WebSocketLike,
444
503
  signals: AbortSignal[],
445
504
  idleTimeoutMs: number,
446
505
  requestFrame: string,
447
506
  onTerminal: (event: Record<string, unknown>) => boolean,
448
507
  onRelease: (keep: boolean) => void,
449
- ): Response {
508
+ allowBootstrapFallback: boolean,
509
+ ): Promise<Response> {
450
510
  const encoder = new TextEncoder();
451
- let cancelBody: (reason?: unknown) => void = () => closeSocket(socket, "consumer_cancelled");
511
+ let resolveReady!: () => void;
512
+ let rejectReady!: (error: unknown) => void;
513
+ const ready = new Promise<void>((resolve, reject) => { resolveReady = resolve; rejectReady = reject; });
514
+ let cancelBody: (reason?: unknown) => void = () => {};
452
515
  const body = new ReadableStream<Uint8Array>({
453
516
  start(controller) {
454
517
  let closed = false;
455
- let released = false;
518
+ let committed = false;
456
519
  let idleTimer: ReturnType<typeof setTimeout> | undefined;
457
- let sawTerminal = false;
520
+ let pending = Promise.resolve();
458
521
  const cleanup = () => {
459
522
  if (idleTimer) clearTimeout(idleTimer);
460
523
  socket.removeEventListener("message", onMessage);
@@ -462,73 +525,75 @@ function createSseResponse(
462
525
  socket.removeEventListener("close", onClose);
463
526
  for (const signal of signals) signal.removeEventListener("abort", onAbort);
464
527
  };
465
- const release = (keep: boolean) => {
466
- if (released) return;
467
- released = true;
468
- onRelease(keep);
469
- };
470
- const finish = () => {
471
- if (closed) return;
472
- closed = true;
473
- cleanup();
474
- controller.close();
475
- };
476
528
  const fail = (error: unknown) => {
477
529
  if (closed) return;
478
530
  closed = true;
479
531
  cleanup();
480
- release(false);
481
- closeSocket(socket, "stream_failed");
532
+ onRelease(false);
533
+ if (!committed) {
534
+ if (signals.some((signal) => signal.aborted) || (allowBootstrapFallback && error instanceof RecoverableWebSocketError)) {
535
+ rejectReady(error);
536
+ } else {
537
+ // Return an errored body for deterministic failures/explicit WS.
538
+ // Rejecting fetch would make the SDK recast these as connection errors.
539
+ resolveReady();
540
+ }
541
+ }
482
542
  controller.error(error);
483
543
  };
484
544
  const armIdleTimer = () => {
485
545
  if (idleTimeoutMs <= 0) return;
486
546
  if (idleTimer) clearTimeout(idleTimer);
487
- idleTimer = setTimeout(() => fail(new Error(`WebSocket idle timeout after ${idleTimeoutMs}ms`)), idleTimeoutMs);
488
- };
489
- const onMessage = (event: WebSocketEvent) => {
490
- void (async () => {
491
- try {
492
- const text = await decodeSocketData(event.data);
493
- if (!text) return;
494
- const parsed: unknown = JSON.parse(text);
495
- if (!isRecord(parsed) || typeof parsed.type !== "string") throw new Error("Invalid Responses WebSocket event");
496
- controller.enqueue(encoder.encode(`event: ${parsed.type}\ndata: ${JSON.stringify(parsed)}\n\n`));
497
- armIdleTimer();
498
- if (TERMINAL_EVENTS.has(parsed.type)) {
499
- sawTerminal = true;
500
- const keep = onTerminal(parsed);
501
- release(keep);
502
- finish();
503
- }
504
- } catch (error) {
505
- fail(error);
506
- }
507
- })();
547
+ idleTimer = setTimeout(() => fail(transportError(`WebSocket idle timeout after ${idleTimeoutMs}ms`)), idleTimeoutMs);
508
548
  };
509
- const onError = (event: WebSocketEvent) => fail(socketError(event));
510
- const onClose = (event: WebSocketEvent) => {
511
- if (sawTerminal) finish();
512
- else fail(new Error(`WebSocket closed before response completion${event.reason ? `: ${event.reason}` : ""}`));
549
+ const enqueue = (operation: () => Promise<void> | void) => {
550
+ pending = pending.then(() => { if (!closed) return operation(); }).catch(fail);
513
551
  };
552
+ const onMessage = (event: WebSocketEvent) => enqueue(async () => {
553
+ const text = await decodeSocketData(event.data);
554
+ if (closed) return;
555
+ if (text === undefined) throw new Error("Invalid Responses WebSocket frame");
556
+ const parsed: unknown = JSON.parse(text);
557
+ if (!isRecord(parsed) || typeof parsed.type !== "string" || !parsed.type) throw new Error("Invalid Responses WebSocket event");
558
+ const normalized = normalizeStreamInterruption(parsed);
559
+ if (!committed && allowBootstrapFallback && normalized) {
560
+ fail(transportError("stream closed before response.completed"));
561
+ return;
562
+ }
563
+ const forwarded = normalized ?? parsed;
564
+ controller.enqueue(encoder.encode(`event: ${forwarded.type}\ndata: ${JSON.stringify(forwarded)}\n\n`));
565
+ committed = true;
566
+ resolveReady();
567
+ armIdleTimer();
568
+ if (TERMINAL_EVENTS.has(parsed.type)) {
569
+ closed = true;
570
+ cleanup();
571
+ const keep = onTerminal(parsed);
572
+ onRelease(keep);
573
+ controller.close();
574
+ }
575
+ });
576
+ const onError = (event: WebSocketEvent) => enqueue(() => fail(socketError(event)));
577
+ const onClose = (event: WebSocketEvent) => enqueue(() => fail(socketCloseError(event, true)));
578
+ // Abort/timeout deliberately bypass decoding: a stuck decoder must not
579
+ // prevent cancellation. Its late result sees closed and is discarded.
514
580
  const onAbort = () => fail(new Error("Request was aborted"));
515
-
516
581
  cancelBody = (reason) => fail(reason instanceof Error ? reason : new Error("Response stream was cancelled"));
517
582
  socket.addEventListener("message", onMessage);
518
583
  socket.addEventListener("error", onError);
519
584
  socket.addEventListener("close", onClose);
520
585
  for (const signal of signals) signal.addEventListener("abort", onAbort, { once: true });
586
+ if (signals.some((signal) => signal.aborted)) { onAbort(); return; }
521
587
  armIdleTimer();
522
588
  try {
523
589
  socket.send(requestFrame);
524
590
  } catch (error) {
525
- fail(error);
591
+ fail(transportError(error));
526
592
  }
527
593
  },
528
- cancel(reason) {
529
- cancelBody(reason);
530
- },
594
+ cancel(reason) { cancelBody(reason); },
531
595
  });
596
+ await ready;
532
597
  return new Response(body, {
533
598
  status: 200,
534
599
  headers: { "content-type": "text/event-stream", connection: "keep-alive" },
@@ -573,12 +638,12 @@ export function createResponsesWebSocketFetch(model: Model<Api>, options: Stream
573
638
  }
574
639
  if (!isRecord(payload)) return fallback(input, init);
575
640
 
576
- const signals = liveSignals(options, init);
641
+ const signals = liveSignals(options, input, init);
577
642
  const requestId = options.sessionId?.slice(0, 64) || crypto.randomUUID();
578
643
  const fetchHeaders = requestHeaders(input, init);
579
644
  const headers = buildWebSocketHeaders(model, options, fetchHeaders, requestId);
580
645
  const cacheKey = shouldCacheSocket(options) ? buildCacheKey(model, options, fetchHeaders) : undefined;
581
- let acquired: AcquiredSocket | undefined;
646
+ let release: ((keep: boolean) => void) | undefined;
582
647
  try {
583
648
  const lease = await acquireWebSocket(
584
649
  buildResponsesWebSocketUrl(model),
@@ -588,21 +653,27 @@ export function createResponsesWebSocketFetch(model: Model<Api>, options: Stream
588
653
  cacheKey,
589
654
  options.sessionId,
590
655
  );
591
- acquired = lease;
656
+ let released = false;
657
+ release = (keep) => {
658
+ if (released) return;
659
+ released = true;
660
+ lease.release(keep);
661
+ };
592
662
  const fullPayload = payload;
593
663
  const requestPayload = lease.entry ? buildCachedRequestBody(lease.entry, fullPayload) : fullPayload;
594
664
  const requestFrame = JSON.stringify({ type: "response.create", ...requestPayload });
595
- return createSseResponse(
665
+ return await createSseResponse(
596
666
  lease.socket,
597
667
  signals,
598
668
  options.timeoutMs ?? 0,
599
669
  requestFrame,
600
670
  (event) => completedContinuation(lease.entry, fullPayload, event),
601
- (keep) => lease.release(keep),
671
+ release,
672
+ transportMode(options) === "auto",
602
673
  );
603
674
  } catch (error) {
604
- acquired?.release(false);
605
- if (transportMode(options) === "auto" && !signals.some((signal) => signal.aborted)) {
675
+ release?.(false);
676
+ if (error instanceof RecoverableWebSocketError && transportMode(options) === "auto" && !signals.some((signal) => signal.aborted)) {
606
677
  return fallback(input, init);
607
678
  }
608
679
  throw error;
package/src/runtime.ts CHANGED
@@ -4,6 +4,7 @@ import { convertToLlm, type ExtensionAPI, type ExtensionContext } from "@earendi
4
4
  import { isExactModelAllowed } from "./model-scope";
5
5
  import {
6
6
  CODEX_AFFINITY_SCOPE,
7
+ buildResponsesRequestHeaders,
7
8
  type CodexAffinity,
8
9
  } from "./responses-headers";
9
10
  import { RESPONSES_COMPACT_CAPABLE_APIS } from "./types";
@@ -86,6 +87,8 @@ export type ResponsesRuntime = {
86
87
  };
87
88
 
88
89
  export type NativeCompactionRuntime = ResponsesRuntime & {
90
+ /** Public host snapshot for the resolved physical model; never inferred from token text. */
91
+ authMode?: "oauth" | "non-oauth" | "unknown";
89
92
  compactPath: string;
90
93
  compactUrl: string;
91
94
  payload?: ResponsesCompatibleRequestPayload;
@@ -503,6 +506,35 @@ async function resolveRequestAuth(ctx: ExtensionContext, model: RuntimeModel): P
503
506
  return modelRegistry.getApiKeyAndHeaders(model);
504
507
  }
505
508
 
509
+ function resolveAuthMode(ctx: ExtensionContext, model: RuntimeModel): NativeCompactionRuntime["authMode"] {
510
+ const registry = ctx.modelRegistry as unknown as {
511
+ isUsingOAuth?: (model: RuntimeModel) => unknown;
512
+ hasConfiguredAuth?: (model: RuntimeModel) => unknown;
513
+ };
514
+ try {
515
+ if (typeof registry.isUsingOAuth !== "function" || typeof registry.hasConfiguredAuth !== "function" ||
516
+ registry.hasConfiguredAuth(model) !== true) return "unknown";
517
+ const oauth = registry.isUsingOAuth(model);
518
+ return oauth === true ? "oauth" : oauth === false ? "non-oauth" : "unknown";
519
+ } catch {
520
+ return "unknown";
521
+ }
522
+ }
523
+
524
+ /** Conservative automatic standalone routing; provider aliases alone prove nothing. */
525
+ export function isStandaloneCompactionEligible(runtime: NativeCompactionRuntime): boolean {
526
+ if (runtime.api !== "openai-responses" || runtime.codexAffinity || runtime.authMode !== "non-oauth") return false;
527
+ try {
528
+ const url = new URL(runtime.compactUrl);
529
+ if (url.protocol !== "https:" || url.hostname !== "api.openai.com" || url.port ||
530
+ url.pathname !== "/v1/responses/compact" || url.username || url.password || url.search || url.hash) return false;
531
+ const headers = buildResponsesRequestHeaders(runtime, { accept: "application/json" });
532
+ return headers.get("authorization") === `Bearer ${runtime.apiKey}`;
533
+ } catch {
534
+ return false;
535
+ }
536
+ }
537
+
506
538
  export function isSupportedApi(api: string): api is ResponsesCompactApi {
507
539
  return (RESPONSES_COMPACT_CAPABLE_APIS as readonly string[]).includes(api);
508
540
  }
@@ -696,6 +728,7 @@ async function resolveNativeCompactionEnvironmentForModel(
696
728
  requestModel,
697
729
  baseUrl,
698
730
  apiKey: auth.apiKey,
731
+ authMode: resolveAuthMode(ctx, effectiveModel),
699
732
  env: auth.env,
700
733
  headers: auth.headers,
701
734
  responsesPath: buildResponsesPath(descriptor.api),
package/src/types.ts CHANGED
@@ -6,7 +6,6 @@ export const TOOLKIT_ID = "pi-openai-toolkit";
6
6
  export const COMPACTION_EXTENSION_ID = `${TOOLKIT_ID}:compaction`;
7
7
  export const WEB_SEARCH_EXTENSION_ID = `${TOOLKIT_ID}:web-search`;
8
8
  export const IMAGE_GENERATION_EXTENSION_ID = `${TOOLKIT_ID}:image-generation`;
9
- export const AUTO_MODE_EXTENSION_ID = `${TOOLKIT_ID}:auto-mode`;
10
9
  export const DEFAULT_ARTIFACT_ROOT = "~/.pi/agent/artifacts/pi-openai-toolkit/compaction";
11
10
  export const REDACTED_VALUE = "[REDACTED]";
12
11
  /**
@@ -43,6 +42,7 @@ export type NativeCompactionInputProvenance =
43
42
  * replay shim; the provider receives the opaque item from details.compactedWindow.
44
43
  */
45
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
46
 
47
47
  export const THINKING_LEVELS: readonly ThinkingLevel[] = [
48
48
  "off",
@@ -165,71 +165,10 @@ export type ImageGenerationConfig = {
165
165
  models: string[];
166
166
  };
167
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
168
  export type ToolkitConfig = {
229
169
  compaction: CompactionConfig;
230
170
  webSearch: WebSearchConfig;
231
171
  imageGeneration: ImageGenerationConfig;
232
- autoMode: AutoModeConfig;
233
172
  };
234
173
 
235
174
  export type LoadedToolkitConfig = {
@@ -324,6 +263,7 @@ export type NativeCompactionDetails = NativeCompactionIdentity & {
324
263
  export type NativeCompactionEntry = CompactionEntry<NativeCompactionDetails>;
325
264
 
326
265
  export type CreateNativeCompactionDetailsInput = NativeCompactionIdentity & {
266
+ strategy?: NativeCompactionStrategy;
327
267
  inputProvenance: NativeCompactionInputProvenance;
328
268
  compactionModel?: NativeCompactionIdentity;
329
269
  deferredToolCarryover?: DeferredToolCarryoverV1;
@@ -338,8 +278,8 @@ export type CreateNativeCompactionResultInput = {
338
278
  tokensBefore: number;
339
279
  details: NativeCompactionDetails;
340
280
  /**
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.
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.
343
283
  */
344
284
  summary?: string;
345
285
  };
@@ -364,7 +304,7 @@ function isStructuredValue(value: unknown): boolean {
364
304
  if (
365
305
  value === null ||
366
306
  typeof value === "string" ||
367
- typeof value === "number" ||
307
+ (typeof value === "number" && Number.isFinite(value)) ||
368
308
  typeof value === "boolean"
369
309
  ) {
370
310
  return true;
@@ -396,17 +336,13 @@ function cloneStructuredValue(value: unknown): unknown {
396
336
  }
397
337
 
398
338
  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;
339
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, cloneStructuredValue(nested)]));
404
340
  }
405
341
 
406
342
  throw new Error(`Unsupported structured value: ${typeof value}`);
407
343
  }
408
344
 
409
- function isCompactedWindowItem(value: unknown): value is Record<string, unknown> {
345
+ export function isCompactedWindowItem(value: unknown): value is Record<string, unknown> {
410
346
  return isRecord(value) && Object.values(value).every(isStructuredValue);
411
347
  }
412
348
 
@@ -533,8 +469,11 @@ export function isNativeCompactionEntry(value: unknown): value is NativeCompacti
533
469
  }
534
470
 
535
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
+ }
536
475
  return {
537
- strategy: NATIVE_COMPACTION_STRATEGY,
476
+ strategy: input.strategy ?? NATIVE_COMPACTION_STRATEGY,
538
477
  inputProvenance: input.inputProvenance,
539
478
  provider: normalizeString(input.provider),
540
479
  api: normalizeString(input.api),
@@ -570,7 +509,9 @@ export function createNativeCompactionResult(
570
509
  ): CompactionResult<NativeCompactionDetails> {
571
510
  const summary = input.summary?.trim();
572
511
  return {
573
- summary: summary && summary.length > 0 ? summary : NATIVE_COMPACTION_FALLBACK_SUMMARY,
512
+ summary: summary && summary.length > 0 ? summary
513
+ : input.details.strategy === LEGACY_NATIVE_COMPACTION_STRATEGY ? STANDALONE_COMPACTION_FALLBACK_SUMMARY
514
+ : NATIVE_COMPACTION_FALLBACK_SUMMARY,
574
515
  firstKeptEntryId: input.firstKeptEntryId,
575
516
  tokensBefore: input.tokensBefore,
576
517
  details: input.details,
@@ -617,32 +558,8 @@ export const DEFAULT_IMAGE_GENERATION_CONFIG: ImageGenerationConfig = {
617
558
  models: [...DEFAULT_IMAGE_GENERATION_MODELS],
618
559
  };
619
560
 
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
561
  export const DEFAULT_TOOLKIT_CONFIG: ToolkitConfig = {
644
562
  compaction: DEFAULT_COMPACTION_CONFIG,
645
563
  webSearch: DEFAULT_WEB_SEARCH_CONFIG,
646
564
  imageGeneration: DEFAULT_IMAGE_GENERATION_CONFIG,
647
- autoMode: DEFAULT_AUTO_MODE_CONFIG,
648
565
  };
@@ -29,10 +29,6 @@
29
29
  "src/image-generation/render.ts",
30
30
  "src/image-generation/service.ts",
31
31
  "src/image-generation/types.ts",
32
- "src/auto-mode/extension.ts",
33
- "src/auto-mode/policy.ts",
34
- "src/auto-mode/reviewer.ts",
35
- "src/auto-mode/types.ts",
36
32
  "src/responses-websocket/extension.ts",
37
33
  "src/responses-websocket/transport.ts",
38
34
  "src/model-scope.ts",