pi-openai-toolkit 0.14.12 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import type { AgentMessage } from "@earendil-works/pi-agent-core";
3
+ import { getCurrentSystemMessage, getCurrentTools } from "@earendil-works/pi-ai";
3
4
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
4
5
  import {
5
6
  CODEX_CONTEXT_WINDOW_MESSAGE_TYPE,
@@ -113,3 +114,125 @@ export function sendContextWindowMessage(
113
114
  ? { deliverAs: "steer", triggerTurn: true }
114
115
  : { triggerTurn: false });
115
116
  }
117
+
118
+ /** Whether a transcript message is a Pi system message (prompt sections and tool deltas). */
119
+ export function isSystemTranscriptMessage(message: AgentMessage): boolean {
120
+ return (message as unknown as { role?: string }).role === "system";
121
+ }
122
+
123
+ /** Tool names the provider would send for this message list, after replaying every delta. */
124
+ export function declaredToolNames(messages: readonly AgentMessage[]): string[] {
125
+ return getCurrentTools(messages).map((tool) => tool.name);
126
+ }
127
+
128
+ export type SystemHeadRepair<TMessage extends AgentMessage = AgentMessage> = {
129
+ /** The window to send: trimmed, trimmed plus a rebuilt head, or untrimmed when the trim is unsafe. */
130
+ readonly messages: readonly TMessage[];
131
+ /** A head from before the boundary was re-anchored at the front of the window. */
132
+ repaired: boolean;
133
+ /** The trim was refused because the window would lose declared tools. */
134
+ shrinkRejected: boolean;
135
+ /** Tool names the trimmed window would have lost; empty on every safe path. */
136
+ lostToolNames: string[];
137
+ /** The system message this window anchors on, when one could be determined. */
138
+ head: AgentMessage | undefined;
139
+ };
140
+
141
+ /**
142
+ * Keep Pi 0.86's prompt and tool head alive across a context-window trim.
143
+ *
144
+ * Pi 0.86 no longer sends the system prompt and the tool loadout beside the
145
+ * transcript: `Agent#createContextSnapshot()` hands over messages only, and the
146
+ * Responses transport rebuilds `params.tools` from the `system` messages inside the
147
+ * message list (`resolveTranscriptTools(getInitialSystemMessage(messages))`). Trimming
148
+ * everything before a window boundary therefore deletes the one message that declares
149
+ * every tool, and Pi does not re-declare it because the persisted transcript still looks
150
+ * unchanged. The result is a window the model can only answer from memory.
151
+ *
152
+ * The merged state of the trimmed-away system messages is re-anchored as the leading
153
+ * message of the window. When even that would lose declared tools, the trim is refused:
154
+ * a window that still carries the previous turns beats a window with no tools at all.
155
+ */
156
+ export function preserveSystemHead<TMessage extends AgentMessage>(args: {
157
+ readonly messages: readonly TMessage[];
158
+ boundaryIndex: number;
159
+ /**
160
+ * Head observed in an earlier projection. A durable compaction can drop the original
161
+ * declaration from the branch, leaving nothing to rebuild from, so the last known head
162
+ * is re-anchored instead of sending a window with no tools.
163
+ */
164
+ fallbackHead?: AgentMessage;
165
+ }): SystemHeadRepair<TMessage> {
166
+ const { messages, boundaryIndex, fallbackHead } = args;
167
+ const window = messages.slice(Math.max(boundaryIndex, 0));
168
+ const prefix = messages.slice(0, Math.max(boundaryIndex, 0));
169
+
170
+ const anchorFallback = (): SystemHeadRepair<TMessage> | undefined => {
171
+ if (!fallbackHead || messages.some(isSystemTranscriptMessage)) return undefined;
172
+ // A boundary compaction can retire the entry that declared the tools, leaving a
173
+ // branch whose only system state is gone. Nothing in the list can be rebuilt, so
174
+ // the head remembered from an earlier projection is re-anchored instead.
175
+ const reused: readonly TMessage[] = [
176
+ fallbackHead as unknown as TMessage,
177
+ ...window.filter((message) => !isSystemTranscriptMessage(message)),
178
+ ];
179
+ return { messages: reused, repaired: true, shrinkRejected: false, lostToolNames: [], head: fallbackHead };
180
+ };
181
+
182
+ if (boundaryIndex <= 0 || prefix.length === 0) {
183
+ return anchorFallback() ?? {
184
+ messages: window,
185
+ repaired: false,
186
+ shrinkRejected: false,
187
+ lostToolNames: [],
188
+ head: getCurrentSystemMessage(messages) ?? fallbackHead,
189
+ };
190
+ }
191
+
192
+ const declared = declaredToolNames(messages);
193
+ const completeHead = getCurrentSystemMessage(messages);
194
+ if (!completeHead) {
195
+ const reused = anchorFallback();
196
+ if (reused) return reused;
197
+ if (!messages.some(isSystemTranscriptMessage) && declared.length === 0) {
198
+ // Pre-0.86 transcripts carry no system messages: the trim stays exactly as before.
199
+ return { messages: window, repaired: false, shrinkRejected: false, lostToolNames: [], head: undefined };
200
+ }
201
+ return { messages, repaired: false, shrinkRejected: true, lostToolNames: declared, head: undefined };
202
+ }
203
+
204
+ const windowFirst = window[0];
205
+ if (windowFirst && isSystemTranscriptMessage(windowFirst)) {
206
+ const kept = new Set(declaredToolNames(window));
207
+ const lostFromWindow = declared.filter((name) => !kept.has(name));
208
+ if (lostFromWindow.length === 0) {
209
+ return {
210
+ messages: window,
211
+ repaired: false,
212
+ shrinkRejected: false,
213
+ lostToolNames: [],
214
+ head: windowFirst as unknown as AgentMessage,
215
+ };
216
+ }
217
+ return {
218
+ messages,
219
+ repaired: false,
220
+ shrinkRejected: true,
221
+ lostToolNames: lostFromWindow,
222
+ head: windowFirst as unknown as AgentMessage,
223
+ };
224
+ }
225
+
226
+ // Later system messages are folded into the rebuilt head, so a patch cannot be read
227
+ // as a complete declaration by a transport that only anchors the first one.
228
+ const rebuilt: readonly TMessage[] = [
229
+ completeHead as unknown as TMessage,
230
+ ...window.filter((message) => !isSystemTranscriptMessage(message)),
231
+ ];
232
+ const after = new Set(declaredToolNames(rebuilt));
233
+ const lostToolNames = declared.filter((name) => !after.has(name));
234
+ if (lostToolNames.length > 0) {
235
+ return { messages, repaired: false, shrinkRejected: true, lostToolNames, head: completeHead };
236
+ }
237
+ return { messages: rebuilt, repaired: true, shrinkRejected: false, lostToolNames: [], head: completeHead };
238
+ }