@nijaru/pi-provider-compaction 0.0.3 → 0.0.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nijaru/pi-provider-compaction",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "type": "module",
5
5
  "description": "Provider-native context compaction adapters for Pi",
6
6
  "keywords": [
@@ -12,7 +12,6 @@
12
12
  "responses"
13
13
  ],
14
14
  "license": "MIT",
15
-
16
15
  "publishConfig": {
17
16
  "access": "public"
18
17
  },
@@ -30,27 +29,29 @@
30
29
  "build": "bun run build.ts",
31
30
  "prepack": "bun run build",
32
31
  "test": "bun test",
33
- "lint": "oxlint build.ts index.ts protocol.ts policy.ts tests/",
32
+ "lint": "oxlint build.ts index.ts protocol.ts policy.ts provider.ts replay.ts tests/",
34
33
  "typecheck": "tsc --noEmit",
35
- "check": "bun test && oxlint build.ts index.ts protocol.ts policy.ts tests/ && tsc --noEmit"
34
+ "check": "bun test && bun run lint && tsc --noEmit"
36
35
  },
37
36
  "files": [
38
37
  "dist",
39
38
  "index.ts",
40
39
  "protocol.ts",
41
40
  "policy.ts",
41
+ "provider.ts",
42
+ "replay.ts",
42
43
  "README.md",
43
44
  "LICENSE"
44
45
  ],
45
46
  "devDependencies": {
46
- "@earendil-works/pi-ai": "0.85.1",
47
- "@earendil-works/pi-agent-core": "0.85.1",
48
- "@earendil-works/pi-coding-agent": "0.85.1",
47
+ "@earendil-works/pi-ai": "0.87.0",
48
+ "@earendil-works/pi-agent-core": "0.87.0",
49
+ "@earendil-works/pi-coding-agent": "0.87.0",
49
50
  "@types/bun": "latest",
50
51
  "oxlint": "^1.81.0",
51
52
  "typescript": "^6.0.3"
52
53
  },
53
54
  "peerDependencies": {
54
- "@earendil-works/pi-coding-agent": ">=0.85.1"
55
+ "@earendil-works/pi-coding-agent": "*"
55
56
  }
56
57
  }
package/protocol.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import type {
2
2
  AssistantMessageEventStream,
3
- Context,
4
3
  Model,
5
4
  Provider,
6
5
  ProviderHeaders,
6
+ TranscriptContext,
7
7
  Usage,
8
8
  } from "@earendil-works/pi-ai";
9
9
 
@@ -46,13 +46,37 @@ function validateItemSize(item: ResponseItem): void {
46
46
  if (byteLength(item) > MAX_ITEM_BYTES) throw new Error("Native compaction output item exceeded 2 MiB");
47
47
  }
48
48
 
49
+ function validContent(value: unknown): boolean {
50
+ return typeof value === "string" || (Array.isArray(value) && value.every((part) => {
51
+ if (!isObject(part)) return false;
52
+ if (part.type === "input_text" || part.type === "output_text" || part.type === "summary_text") return typeof part.text === "string";
53
+ if (part.type === "input_image") return typeof part.image_url === "string" || typeof part.file_id === "string";
54
+ if (part.type === "refusal") return typeof part.refusal === "string";
55
+ return false;
56
+ }));
57
+ }
58
+
49
59
  export function validateCompactedOutput(value: unknown): ResponseItem[] {
50
60
  if (!isObject(value) || !Array.isArray(value.output) || value.output.length === 0) {
51
61
  throw new Error("Responses Compact returned an invalid output window");
52
62
  }
63
+ if (byteLength(value.output) > MAX_RESPONSE_BYTES) throw new Error("Native compaction output exceeded 8 MiB");
53
64
  const output = value.output.map((item) => {
54
65
  if (!isObject(item)) throw new Error("Responses Compact returned a non-object output item");
55
66
  validateItemSize(item);
67
+ const type = item.type ?? (typeof item.role === "string" ? "message" : undefined);
68
+ if (!["compaction", "message", "reasoning", "function_call", "function_call_output", "custom_tool_call", "custom_tool_call_output", "tool_search_call", "tool_search_output", "additional_tools"].includes(String(type))) throw new Error("Responses Compact returned an unsupported output item");
69
+ if (type === "message" && (!["user", "assistant", "system", "developer"].includes(String(item.role)) || !validContent(item.content))) throw new Error("Responses Compact returned an invalid message item");
70
+ if (type === "compaction" && !isCompactionItem(item)) throw new Error("Responses Compact returned an invalid checkpoint");
71
+ if (type === "reasoning" && !(typeof item.encrypted_content === "string" || (Array.isArray(item.summary) && validContent(item.summary)))) throw new Error("Responses Compact returned invalid reasoning");
72
+ if (String(type).includes("call") || type === "tool_search_output") {
73
+ if (typeof item.call_id !== "string" || !item.call_id) throw new Error("Responses Compact returned an invalid tool id");
74
+ if (type === "function_call" && (typeof item.name !== "string" || typeof item.arguments !== "string")) throw new Error("Responses Compact returned an invalid function call");
75
+ if (type === "custom_tool_call" && (typeof item.name !== "string" || typeof item.input !== "string")) throw new Error("Responses Compact returned an invalid custom call");
76
+ if ((type === "function_call_output" || type === "custom_tool_call_output") && !validContent(item.output)) throw new Error("Responses Compact returned an invalid tool output");
77
+ if (type === "tool_search_call" && !isObject(item.arguments)) throw new Error("Responses Compact returned an invalid tool search");
78
+ }
79
+ if ((type === "additional_tools" || type === "tool_search_output") && (!Array.isArray(item.tools) || !item.tools.every(isObject))) throw new Error("Responses Compact returned invalid tools");
56
80
  return structuredClone(item);
57
81
  });
58
82
  const checkpoints = output.filter(isCompactionItem);
@@ -85,6 +109,7 @@ async function readJsonBounded(response: Response, signal: AbortSignal): Promise
85
109
  }
86
110
  } finally {
87
111
  signal.removeEventListener("abort", onAbort);
112
+ void reader.cancel().catch(() => undefined);
88
113
  reader.releaseLock();
89
114
  }
90
115
  const bytes = new Uint8Array(total);
@@ -193,8 +218,10 @@ async function collectProviderUsage(stream: AssistantMessageEventStream, signal:
193
218
  interface TransportRequest {
194
219
  provider: Provider;
195
220
  model: Model<any>;
196
- context: Context;
221
+ context: TranscriptContext;
197
222
  input: ResponseItem[];
223
+ /** Fully prepared request snapshot; only its validated prefix is sent. */
224
+ preparedPayload?: JsonObject;
198
225
  apiKey?: string;
199
226
  headers?: ProviderHeaders;
200
227
  env?: Record<string, string>;
@@ -251,7 +278,8 @@ async function requestResponsesCompact(request: TransportRequest): Promise<Trans
251
278
  fetch: bridgeFetch,
252
279
  onPayload: (payload: unknown) => {
253
280
  if (!isObject(payload)) throw new Error("Responses provider exposed a non-object payload");
254
- preparedPayload = { ...structuredClone(payload), input: structuredClone(request.input) };
281
+ if (request.preparedPayload && request.preparedPayload.model !== payload.model) throw new Error("Native route model changed since capture");
282
+ preparedPayload = { ...structuredClone(request.preparedPayload ?? payload), input: structuredClone(request.input) };
255
283
  return preparedPayload;
256
284
  },
257
285
  });
@@ -325,6 +353,7 @@ async function collectCompactionSse(stream: ReadableStream<Uint8Array>, signal:
325
353
  dispatch();
326
354
  } finally {
327
355
  signal.removeEventListener("abort", onAbort);
356
+ void reader.cancel().catch(() => undefined);
328
357
  reader.releaseLock();
329
358
  }
330
359
  if (!completed) throw new Error("Remote V2 stream ended without response.completed");
@@ -359,12 +388,16 @@ function buildRemoteV2History(input: ResponseItem[], item: ResponseItem): Respon
359
388
  async function requestRemoteV2(request: TransportRequest): Promise<TransportResponse> {
360
389
  const baseFetch = request.fetch ?? globalThis.fetch;
361
390
  let sentInput: ResponseItem[] | undefined;
362
- const inspections: Promise<CollectedSse>[] = [];
391
+ const controller = new AbortController();
392
+ const signal = AbortSignal.any([request.signal, controller.signal]);
393
+ const inspections: Promise<{ result: CollectedSse } | { error: unknown }>[] = [];
363
394
  const inspectedFetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
364
395
  const response = await baseFetch(input, init);
365
396
  if (!response.ok || !response.body) return response;
366
397
  const [providerBody, inspectionBody] = response.body.tee();
367
- inspections.push(collectCompactionSse(inspectionBody, request.signal));
398
+ // Attach the rejection handler immediately: inspection can fail before the
399
+ // provider finishes consuming its half of the stream.
400
+ inspections.push(collectCompactionSse(inspectionBody, signal).then((result) => ({ result }), (error: unknown) => ({ error })));
368
401
  return new Response(providerBody, {
369
402
  status: response.status,
370
403
  statusText: response.statusText,
@@ -384,19 +417,24 @@ async function requestRemoteV2(request: TransportRequest): Promise<TransportResp
384
417
  onPayload: (payload: unknown) => {
385
418
  if (!isObject(payload)) throw new Error("Codex provider exposed a non-object payload");
386
419
  sentInput = structuredClone(request.input);
387
- return { ...structuredClone(payload), input: [...sentInput, { type: "compaction_trigger" }] };
420
+ if (request.preparedPayload && request.preparedPayload.model !== payload.model) throw new Error("Native route model changed since capture");
421
+ return { ...structuredClone(request.preparedPayload ?? payload), stream: true, input: [...sentInput, { type: "compaction_trigger" }] };
388
422
  },
389
423
  });
390
- const usage = await collectProviderUsage(stream, request.signal);
391
- if (!sentInput || inspections.length !== 1) {
392
- throw new Error(`Remote V2 observed ${inspections.length} successful provider responses; expected exactly one`);
424
+ try {
425
+ const usage = await collectProviderUsage(stream, request.signal);
426
+ if (!sentInput || inspections.length !== 1) throw new Error(`Remote V2 observed ${inspections.length} successful provider responses; expected exactly one`);
427
+ const inspection = await inspections[0]!;
428
+ if ("error" in inspection) throw inspection.error;
429
+ return { output: buildRemoteV2History(sentInput, inspection.result.item), usage };
430
+ } finally {
431
+ controller.abort();
393
432
  }
394
- const inspection = await inspections[0];
395
- return { output: buildRemoteV2History(sentInput, inspection.item), usage };
396
433
  }
397
434
 
398
435
  export async function requestProviderCompaction(
399
436
  request: TransportRequest & { protocol: NativeProtocol },
400
437
  ): Promise<TransportResponse> {
401
- return request.protocol === "remote-v2" ? requestRemoteV2(request) : requestResponsesCompact(request);
438
+ const result = await (request.protocol === "remote-v2" ? requestRemoteV2(request) : requestResponsesCompact(request));
439
+ return { ...result, output: validateCompactedOutput(result) };
402
440
  }
package/provider.ts ADDED
@@ -0,0 +1,85 @@
1
+ import type { Provider, StreamOptions, Model, TranscriptContext } from "@earendil-works/pi-ai";
2
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+
4
+ export type ObserveRequest = (model: Model<any>, context: TranscriptContext, options: StreamOptions) => ((payload: unknown) => unknown | Promise<unknown>) | undefined;
5
+
6
+ function optionsWithObserver<T extends StreamOptions>(model: Model<any>, context: TranscriptContext, options: T | undefined, observe: ObserveRequest): T | undefined {
7
+ const handler = observe(model, context, options ?? {});
8
+ if (!handler) return options;
9
+ return {
10
+ ...options,
11
+ onPayload: async (payload: unknown, requestModel: Model<any>) => {
12
+ const replacement = await options?.onPayload?.(payload, requestModel);
13
+ const prepared = replacement === undefined ? payload : replacement;
14
+ // Observation failures must not suppress or alter the portable request.
15
+ try { return await handler(prepared); } catch { return prepared; }
16
+ },
17
+ } as T;
18
+ }
19
+
20
+ /** Preserve method receivers, getters, optional members and dynamic catalogs. */
21
+ export function decorateProvider(base: Provider, observe: ObserveRequest): Provider {
22
+ const stream: Provider["stream"] = (model, context, options) => base.stream(model, context, optionsWithObserver(model, context, options, observe));
23
+ const streamSimple: Provider["streamSimple"] = (model, context, options) => base.streamSimple(model, context, optionsWithObserver(model, context, options, observe));
24
+ const bound = new Map<PropertyKey, unknown>();
25
+ return new Proxy(base, {
26
+ get(target, key) {
27
+ if (key === "stream") return stream;
28
+ if (key === "streamSimple") return streamSimple;
29
+ const value = Reflect.get(target, key, target);
30
+ if (typeof value !== "function") return value;
31
+ if (!bound.has(key)) bound.set(key, value.bind(target));
32
+ return bound.get(key);
33
+ },
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Pi 0.87 has replacement registration, not middleware. Native BASE providers can
39
+ * be decorated directly. Legacy stream overlays retain their configuration and
40
+ * delegate their original callback (including /fast). Never register an effective
41
+ * composed provider as a native base: that loses model headers and freezes config.
42
+ */
43
+ export function installObserver(pi: ExtensionAPI, ctx: ExtensionContext, observe: ObserveRequest): (() => void) | undefined {
44
+ const model = ctx.model;
45
+ if (!model) return;
46
+ const registry = ctx.modelRegistry;
47
+ const id = model.provider;
48
+ const native = registry.getRegisteredNativeProvider(id);
49
+ if (native) {
50
+ const guarded: ObserveRequest = (...args) => {
51
+ const handler = observe(...args);
52
+ return handler ? (payload) => registry.getRegisteredNativeProvider(id) === wrapper ? handler(payload) : payload : undefined;
53
+ };
54
+ const wrapper = decorateProvider(native, guarded);
55
+ pi.registerProvider(wrapper);
56
+ return () => {
57
+ if (registry.getRegisteredNativeProvider(id) === wrapper) pi.registerProvider(native);
58
+ };
59
+ }
60
+ const original = registry.getRegisteredProviderConfig(id);
61
+ // Refresh callbacks own mutable catalog state inside the composer. Replacing
62
+ // that composer cannot promise preservation, so leave it alone.
63
+ if (!original?.streamSimple || original.refreshModels || original.oauth?.modifyModels) return;
64
+ if (original.api !== model.api) return;
65
+ // An injected legacy streamSimple overlay would also intercept full stream()
66
+ // calls and change reasoning semantics. Only decorate an existing stream owner.
67
+ const delegate = original.streamSimple.bind(original);
68
+ const guarded: ObserveRequest = (...args) => {
69
+ const handler = observe(...args);
70
+ return handler ? (payload) => registry.getRegisteredProviderConfig(id)?.streamSimple === wrapper ? handler(payload) : payload : undefined;
71
+ };
72
+ const wrapper: Provider["streamSimple"] = (requestModel, context, options) => delegate(requestModel, context, optionsWithObserver(requestModel, context, options, guarded));
73
+ pi.registerProvider(id, { ...original, api: original?.api ?? model.api, streamSimple: wrapper });
74
+ return () => {
75
+ const current = registry.getRegisteredProviderConfig(id);
76
+ if (current?.streamSimple !== wrapper || registry.getRegisteredNativeProvider(id)) return;
77
+ // registerProvider merges undefined properties: remove our layer before restore.
78
+ const restored = { ...current };
79
+ if (original?.streamSimple) restored.streamSimple = original.streamSimple;
80
+ else delete restored.streamSimple;
81
+ if (!original?.api && restored.api === model.api) delete restored.api;
82
+ pi.unregisterProvider(id);
83
+ if (Object.keys(restored).length) pi.registerProvider(id, restored);
84
+ };
85
+ }
package/replay.ts ADDED
@@ -0,0 +1,140 @@
1
+ import type { Model, TranscriptContext } from "@earendil-works/pi-ai";
2
+ import { getCurrentSystemPrompt, getCurrentTools, getDeclaredTools, normalizeContext, resolveTranscript } from "@earendil-works/pi-ai";
3
+ import { createGrammarToolInputProperties } from "@earendil-works/pi-ai/api/constrained-sampling";
4
+ import { convertResponsesMessages } from "@earendil-works/pi-ai/api/openai-responses-shared";
5
+ import { isObject, type JsonObject, type ResponseItem } from "./protocol";
6
+
7
+ const TOOL_CALL_PROVIDERS = new Set(["openai", "openai-codex", "opencode"]);
8
+ export const MAX_SNAPSHOT_BYTES = 8 * 1024 * 1024;
9
+ export const SNAPSHOT_TTL_MS = 5 * 60_000;
10
+
11
+ export function equal(left: unknown, right: unknown): boolean {
12
+ return JSON.stringify(left) === JSON.stringify(right);
13
+ }
14
+
15
+ /** Validator only. No serialized item from this function is transmitted. */
16
+ export function serializeInput(model: Model<any>, context: TranscriptContext, declarations = context): ResponseItem[] {
17
+ const compat = model.compat as Record<string, unknown> | undefined;
18
+ const flag = (name: string) => compat?.[name] === true;
19
+ const codex = model.api === "openai-codex-responses";
20
+ const normalized = resolveTranscript(context, flag("supportsMidConvoSystemMessages"));
21
+ const tools = resolveTranscript(declarations, flag("supportsMidConvoSystemMessages"));
22
+ return convertResponsesMessages(model, normalized, model.api === "azure-openai-responses" ? new Set([...TOOL_CALL_PROVIDERS, "azure-openai-responses"]) : TOOL_CALL_PROVIDERS, {
23
+ includeSystemPrompt: !codex,
24
+ grammarToolInputProperties: createGrammarToolInputProperties(getDeclaredTools(tools.messages), flag("supportsOpenAIGrammarTools")),
25
+ supportsMidConvoSystemMessages: flag("supportsMidConvoSystemMessages"),
26
+ supportsAdditionalTools: flag("supportsAdditionalTools"),
27
+ supportsToolSearch: flag("supportsToolSearch"),
28
+ toolOptions: {
29
+ ...(codex ? { strict: null } : {}),
30
+ supportsStrictMode: (compat?.supportsStrictMode as boolean | undefined) ?? model.api !== "openai-responses",
31
+ supportsOpenAIGrammarTools: flag("supportsOpenAIGrammarTools"),
32
+ },
33
+ }) as unknown as ResponseItem[];
34
+ }
35
+
36
+ export interface PreparedRequest {
37
+ context: TranscriptContext;
38
+ canonical: TranscriptContext;
39
+ payload: JsonObject & { input: ResponseItem[] };
40
+ /** The portable wire input before our sole summary substitution. */
41
+ portableInput: ResponseItem[];
42
+ replacement?: { index: number; count: number };
43
+ createdAt: number;
44
+ }
45
+
46
+ export function capturePreparedRequest(
47
+ model: Model<any>, context: TranscriptContext, canonical: TranscriptContext, payload: unknown,
48
+ reject: (reason: string) => void = () => {},
49
+ ): PreparedRequest | undefined {
50
+ if (!isObject(payload) || !Array.isArray(payload.input) || !payload.input.every(isObject)) { reject("unsupported payload shape"); return; }
51
+ // Reject stateful provider requests: coverage must be the complete visible input.
52
+ if (payload.previous_response_id || payload.conversation) { reject("stateful request"); return; }
53
+ if (Buffer.byteLength(JSON.stringify({ context, payload })) > MAX_SNAPSHOT_BYTES) { reject("snapshot exceeds size limit"); return; }
54
+ const expected = serializeInput(model, context);
55
+ if (!equal(expected, payload.input)) { reject("prepared input differs from serializer"); return; }
56
+ return { context: structuredClone(context), canonical: structuredClone(canonical), payload: structuredClone(payload) as PreparedRequest["payload"], portableInput: expected, createdAt: Date.now() };
57
+ }
58
+
59
+ function promptCount(input: readonly ResponseItem[]): number {
60
+ let count = 0;
61
+ while (input[count]?.role === "system" || input[count]?.role === "developer") count++;
62
+ return count;
63
+ }
64
+
65
+ /** No open/synthetic tool exchange may cross the coverage boundary. */
66
+ function closedTools(context: TranscriptContext): boolean {
67
+ const pending = new Set<string>();
68
+ for (const message of context.messages) {
69
+ if (message.role === "assistant") {
70
+ if (message.stopReason === "error" || message.stopReason === "aborted") return false;
71
+ for (const block of message.content) if (block.type === "toolCall") {
72
+ if (pending.has(block.id)) return false;
73
+ pending.add(block.id);
74
+ }
75
+ } else if (message.role === "toolResult") {
76
+ if (!pending.delete(message.toolCallId)) return false;
77
+ }
78
+ }
79
+ return pending.size === 0;
80
+ }
81
+
82
+ /** Only the head may differ (forced/request-only prompt); conversation mapping is exact. */
83
+ function mappedPrefix(snapshot: PreparedRequest, prefix: TranscriptContext): TranscriptContext | undefined {
84
+ if (!equal(snapshot.canonical.messages.slice(0, prefix.messages.length), prefix.messages)) return;
85
+ const head = prefix.messages[0]?.role === "system" ? 1 : 0;
86
+ if (head && snapshot.context.messages[0]?.role !== "system") return;
87
+ if (!equal(snapshot.context.messages.slice(head, prefix.messages.length), prefix.messages.slice(head))) return;
88
+ return normalizeContext({ messages: snapshot.context.messages.slice(0, prefix.messages.length) });
89
+ }
90
+
91
+ export function selectCoveredPrefix(model: Model<any>, snapshot: PreparedRequest, prefix: TranscriptContext, reject: (reason: string) => void = () => {}): ResponseItem[] | undefined {
92
+ if (Date.now() - snapshot.createdAt > SNAPSHOT_TTL_MS) { reject("snapshot expired"); return; }
93
+ if (!closedTools(prefix)) { reject("prefix has an incomplete tool exchange or failed assistant"); return; }
94
+ const mapped = mappedPrefix(snapshot, prefix);
95
+ if (!mapped) { reject("projected prefix differs from request context"); return; }
96
+ const expected = serializeInput(model, mapped, snapshot.context);
97
+ if (!equal(snapshot.portableInput.slice(0, expected.length), expected)) { reject("serialized prefix does not match prepared input"); return; }
98
+ const start = promptCount(expected);
99
+ let end = expected.length;
100
+ if (snapshot.replacement) {
101
+ if (snapshot.replacement.index < start || snapshot.replacement.index >= end) { reject("previous native slot is outside covered prefix"); return; }
102
+ end += snapshot.replacement.count - 1;
103
+ }
104
+ if (end <= start) { reject("empty covered prefix"); return; }
105
+ // Source is the prepared request, never the validator's reconstruction.
106
+ return structuredClone(snapshot.payload.input.slice(start, end));
107
+ }
108
+
109
+ /** Exact positional replacement, not a subsequence search or a suffix rebuild. */
110
+ export function substituteSummary(
111
+ payload: unknown, index: number, summary: ResponseItem, output: ResponseItem[],
112
+ ): (JsonObject & { input: ResponseItem[] }) | undefined {
113
+ if (!isObject(payload) || !Array.isArray(payload.input) || !payload.input.every(isObject)) return;
114
+ if (!equal(payload.input[index], summary)) return;
115
+ if (payload.input.filter((item) => equal(item, summary)).length !== 1) return;
116
+ return { ...payload, input: [...payload.input.slice(0, index), ...structuredClone(output), ...payload.input.slice(index + 1)] };
117
+ }
118
+
119
+ /** Pin the one portable-summary slot using the canonical prefix, not text search. */
120
+ export function summarySlot(model: Model<any>, snapshot: PreparedRequest, summaryPrefix: TranscriptContext): number | undefined {
121
+ const mapped = mappedPrefix(snapshot, summaryPrefix);
122
+ if (!mapped) return;
123
+ const prefix = serializeInput(model, mapped, snapshot.context);
124
+ const index = promptCount(prefix);
125
+ if (prefix.length !== index + 1 || !equal(snapshot.portableInput.slice(0, prefix.length), prefix)) return;
126
+ return index;
127
+ }
128
+
129
+ /** Changes to prompt, tools or model compatibility revoke opaque state. */
130
+ export function requestPolicy(model: Model<any>, snapshot: PreparedRequest): unknown {
131
+ return {
132
+ model,
133
+ system: getCurrentSystemPrompt(snapshot.context.messages),
134
+ currentTools: getCurrentTools(snapshot.context.messages),
135
+ declarations: getDeclaredTools(resolveTranscript(snapshot.context, (model.compat as { supportsMidConvoSystemMessages?: boolean } | undefined)?.supportsMidConvoSystemMessages).messages),
136
+ instructions: snapshot.payload.instructions,
137
+ tools: snapshot.payload.tools,
138
+ prompt: snapshot.portableInput.slice(0, promptCount(snapshot.portableInput)),
139
+ };
140
+ }