abelworkflow 1.2.3 → 1.2.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/extensions/gpt-responses-compat.ts +166 -27
- package/package.json +1 -1
|
@@ -8,6 +8,20 @@ const RESPONSES_PROVIDERS = new Set(["abelworkflow", "gpt"]);
|
|
|
8
8
|
const UNSUPPORTED_SPARK_REASONING_EFFORTS = new Set(["none", "minimal"]);
|
|
9
9
|
const TRANSIENT_UPSTREAM_ERROR = /\bupstream(?:[_ -]error|\s+request\s+failed)\b/i;
|
|
10
10
|
const EXPLICIT_ERROR_STATUS = /(?:^\s*|\b(?:upstream(?:[_ -]error|\s+request\s+failed)|code|error(?:[_ -]?code)?|http(?:\/\d+(?:\.\d+)*)?(?:[_ -]?status(?:[_ -]?code)?)?|status(?:[_ -]?code)?)\b\s*["']?\s*[:=(]?\s*)["']?([45]\d{2})["']?(?!\d)/i;
|
|
11
|
+
const SUMMARIZATION_SYSTEM_PROMPT = "You summarize coding conversations into concise continuation checkpoints. Output only the requested structured summary.";
|
|
12
|
+
const SUMMARIZATION_PROMPT = `Create a structured checkpoint using exactly these sections:
|
|
13
|
+
|
|
14
|
+
## Goal
|
|
15
|
+
## Constraints & Preferences
|
|
16
|
+
## Progress
|
|
17
|
+
### Done
|
|
18
|
+
### In Progress
|
|
19
|
+
### Blocked
|
|
20
|
+
## Key Decisions
|
|
21
|
+
## Next Steps
|
|
22
|
+
## Critical Context
|
|
23
|
+
|
|
24
|
+
Preserve exact file paths, function names, decisions, errors, and unfinished work. Be concise but include everything needed to continue.`;
|
|
11
25
|
|
|
12
26
|
function isRecord(value: unknown): value is JsonRecord {
|
|
13
27
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
@@ -98,9 +112,13 @@ function normalizeInputItem(item: unknown): unknown {
|
|
|
98
112
|
if (item.type === "reasoning") return undefined;
|
|
99
113
|
|
|
100
114
|
if (item.type === "message") {
|
|
101
|
-
const
|
|
115
|
+
const role = item.role === "user" ? "user" : item.role === "assistant" ? "assistant" : undefined;
|
|
116
|
+
if (!role) return undefined;
|
|
117
|
+
const content = role === "user"
|
|
118
|
+
? normalizeUserContent(item.content)
|
|
119
|
+
: normalizeAssistantContent(item.content);
|
|
102
120
|
if (Array.isArray(content) && content.length === 0) return undefined;
|
|
103
|
-
return { role
|
|
121
|
+
return { role, content };
|
|
104
122
|
}
|
|
105
123
|
|
|
106
124
|
if (item.role === "user") {
|
|
@@ -157,32 +175,153 @@ function normalizeReasoning(payload: JsonRecord, modelId: string | undefined): v
|
|
|
157
175
|
payload.reasoning = { ...payload.reasoning, effort: "low" };
|
|
158
176
|
}
|
|
159
177
|
|
|
178
|
+
function serializeConversation(messages: unknown[]): string {
|
|
179
|
+
return messages.map((message) => {
|
|
180
|
+
if (!isRecord(message)) return "";
|
|
181
|
+
const content = textFromContent(message.content) ?? "";
|
|
182
|
+
if (message.role === "user") return content ? `[User]: ${content}` : "";
|
|
183
|
+
if (message.role === "toolResult") {
|
|
184
|
+
const text = content.length > 2000 ? `${content.slice(0, 2000)}\n[truncated]` : content;
|
|
185
|
+
return text ? `[Tool result]: ${text}` : "";
|
|
186
|
+
}
|
|
187
|
+
if (message.role === "assistant" && Array.isArray(message.content)) {
|
|
188
|
+
const parts: string[] = [];
|
|
189
|
+
const thinking = message.content
|
|
190
|
+
.filter((part) => isRecord(part) && part.type === "thinking" && typeof part.thinking === "string")
|
|
191
|
+
.map((part) => part.thinking)
|
|
192
|
+
.join("\n");
|
|
193
|
+
if (thinking) parts.push(`[Assistant thinking]: ${thinking}`);
|
|
194
|
+
if (content) parts.push(`[Assistant]: ${content}`);
|
|
195
|
+
const calls = message.content
|
|
196
|
+
.filter((part) => isRecord(part) && part.type === "toolCall")
|
|
197
|
+
.map((part) => `${part.name}(${JSON.stringify(part.arguments ?? {})})`)
|
|
198
|
+
.join("; ");
|
|
199
|
+
if (calls) parts.push(`[Assistant tool calls]: ${calls}`);
|
|
200
|
+
return parts.join("\n");
|
|
201
|
+
}
|
|
202
|
+
if (message.role === "bashExecution") {
|
|
203
|
+
return `[Bash]: ${String(message.command ?? "")}\n${String(message.output ?? "")}`;
|
|
204
|
+
}
|
|
205
|
+
if (typeof message.summary === "string") return `[Previous checkpoint]: ${message.summary}`;
|
|
206
|
+
return content;
|
|
207
|
+
}).filter(Boolean).join("\n\n");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function fileDetails(fileOps: unknown): {
|
|
211
|
+
readFiles: string[];
|
|
212
|
+
modifiedFiles: string[];
|
|
213
|
+
} {
|
|
214
|
+
if (!isRecord(fileOps)) return { readFiles: [], modifiedFiles: [] };
|
|
215
|
+
const modified = new Set<string>([
|
|
216
|
+
...(fileOps.edited instanceof Set ? fileOps.edited : []),
|
|
217
|
+
...(fileOps.written instanceof Set ? fileOps.written : []),
|
|
218
|
+
]);
|
|
219
|
+
return {
|
|
220
|
+
readFiles: [...(fileOps.read instanceof Set ? fileOps.read : [])]
|
|
221
|
+
.filter((path): path is string => typeof path === "string" && !modified.has(path))
|
|
222
|
+
.sort(),
|
|
223
|
+
modifiedFiles: [...modified].sort(),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function formatFileDetails(details: { readFiles: string[]; modifiedFiles: string[] }): string {
|
|
228
|
+
const sections: string[] = [];
|
|
229
|
+
if (details.readFiles.length > 0) {
|
|
230
|
+
sections.push(`<read-files>\n${details.readFiles.join("\n")}\n</read-files>`);
|
|
231
|
+
}
|
|
232
|
+
if (details.modifiedFiles.length > 0) {
|
|
233
|
+
sections.push(`<modified-files>\n${details.modifiedFiles.join("\n")}\n</modified-files>`);
|
|
234
|
+
}
|
|
235
|
+
return sections.length > 0 ? `\n\n${sections.join("\n\n")}` : "";
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function normalizeResponsesPayload(
|
|
239
|
+
payload: unknown,
|
|
240
|
+
model: unknown,
|
|
241
|
+
systemPrompt: string,
|
|
242
|
+
): JsonRecord | undefined {
|
|
243
|
+
if (!isRecord(payload) || !isCompatibleResponsesModel(model)) return undefined;
|
|
244
|
+
|
|
245
|
+
const input = normalizeInput(payload.input);
|
|
246
|
+
const first = Array.isArray(payload.input) ? payload.input.find(isRecord) : undefined;
|
|
247
|
+
const firstIsPrompt = first?.role === "system" || first?.role === "developer";
|
|
248
|
+
const instructions =
|
|
249
|
+
textFromContent(payload.instructions) ||
|
|
250
|
+
(firstIsPrompt ? textFromContent(first.content) : undefined) ||
|
|
251
|
+
systemPrompt.trim() ||
|
|
252
|
+
DEFAULT_INSTRUCTIONS;
|
|
253
|
+
const nextPayload: JsonRecord = { ...payload, instructions, input, store: false };
|
|
254
|
+
|
|
255
|
+
delete nextPayload.max_output_tokens;
|
|
256
|
+
delete nextPayload.prompt_cache_key;
|
|
257
|
+
delete nextPayload.prompt_cache_retention;
|
|
258
|
+
delete nextPayload.previous_response_id;
|
|
259
|
+
|
|
260
|
+
normalizeReasoning(nextPayload, isRecord(model) && typeof model.id === "string" ? model.id : undefined);
|
|
261
|
+
return nextPayload;
|
|
262
|
+
}
|
|
263
|
+
|
|
160
264
|
export default function (pi: ExtensionAPI) {
|
|
161
|
-
pi.on("before_provider_request", (event, ctx) =>
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
265
|
+
pi.on("before_provider_request", (event, ctx) =>
|
|
266
|
+
normalizeResponsesPayload(event.payload, ctx.model, ctx.getSystemPrompt()));
|
|
267
|
+
|
|
268
|
+
pi.on("session_before_compact", async (event, ctx) => {
|
|
269
|
+
const model = ctx.model;
|
|
270
|
+
if (!isCompatibleResponsesModel(model)) return;
|
|
271
|
+
|
|
272
|
+
const { preparation } = event;
|
|
273
|
+
const conversation = serializeConversation([
|
|
274
|
+
...preparation.messagesToSummarize,
|
|
275
|
+
...preparation.turnPrefixMessages,
|
|
276
|
+
]);
|
|
277
|
+
const prompt = [
|
|
278
|
+
`<conversation>\n${conversation}\n</conversation>`,
|
|
279
|
+
preparation.previousSummary
|
|
280
|
+
? `<previous-summary>\n${preparation.previousSummary}\n</previous-summary>\nPreserve and update this previous checkpoint.`
|
|
281
|
+
: "",
|
|
282
|
+
preparation.isSplitTurn
|
|
283
|
+
? "The conversation ends with the prefix of a split turn; its recent suffix remains in context."
|
|
284
|
+
: "",
|
|
285
|
+
SUMMARIZATION_PROMPT,
|
|
286
|
+
event.customInstructions ? `Additional focus: ${event.customInstructions}` : "",
|
|
287
|
+
].filter(Boolean).join("\n\n");
|
|
288
|
+
const response = await ctx.modelRegistry.complete(
|
|
289
|
+
model,
|
|
290
|
+
{
|
|
291
|
+
systemPrompt: SUMMARIZATION_SYSTEM_PROMPT,
|
|
292
|
+
messages: [{
|
|
293
|
+
role: "user",
|
|
294
|
+
content: [{ type: "text", text: prompt }],
|
|
295
|
+
timestamp: Date.now(),
|
|
296
|
+
}],
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
signal: event.signal,
|
|
300
|
+
cacheRetention: "none",
|
|
301
|
+
...(ctx.thinkingLevel && ctx.thinkingLevel !== "off"
|
|
302
|
+
? { reasoningEffort: ctx.thinkingLevel }
|
|
303
|
+
: {}),
|
|
304
|
+
onPayload: (payload: unknown, payloadModel: unknown) =>
|
|
305
|
+
normalizeResponsesPayload(payload, payloadModel, SUMMARIZATION_SYSTEM_PROMPT),
|
|
306
|
+
},
|
|
307
|
+
);
|
|
308
|
+
if (response.stopReason === "error") {
|
|
309
|
+
throw new Error(`Summarization failed: ${response.errorMessage ?? "Unknown error"}`);
|
|
310
|
+
}
|
|
311
|
+
if (response.stopReason === "aborted") throw new Error("Compaction cancelled");
|
|
312
|
+
|
|
313
|
+
const summaryText = textFromContent(response.content);
|
|
314
|
+
if (!summaryText) throw new Error("Summarization failed: Empty response");
|
|
315
|
+
const details = fileDetails(preparation.fileOps);
|
|
316
|
+
return {
|
|
317
|
+
compaction: {
|
|
318
|
+
summary: `${summaryText}${formatFileDetails(details)}`,
|
|
319
|
+
firstKeptEntryId: preparation.firstKeptEntryId,
|
|
320
|
+
tokensBefore: preparation.tokensBefore,
|
|
321
|
+
usage: response.usage,
|
|
322
|
+
details,
|
|
323
|
+
},
|
|
324
|
+
};
|
|
186
325
|
});
|
|
187
326
|
|
|
188
327
|
pi.on("message_end", (event, ctx) => {
|