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.
@@ -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 content = normalizeAssistantContent(item.content);
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: "assistant", content };
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
- const payload = event.payload as unknown;
163
- if (!isRecord(payload)) return;
164
- if (!isCompatibleResponsesModel(ctx.model)) return;
165
-
166
- const input = normalizeInput(payload.input);
167
- const first = Array.isArray(payload.input) ? payload.input.find(isRecord) : undefined;
168
- const firstIsPrompt = first?.role === "system" || first?.role === "developer";
169
- const instructions =
170
- textFromContent(payload.instructions) ||
171
- (firstIsPrompt ? textFromContent(first.content) : undefined) ||
172
- ctx.getSystemPrompt().trim() ||
173
- DEFAULT_INSTRUCTIONS;
174
-
175
- const nextPayload: JsonRecord = { ...payload, instructions, input, store: false };
176
-
177
- // Unsupported or risky for this Codex passthrough endpoint.
178
- delete nextPayload.max_output_tokens;
179
- delete nextPayload.prompt_cache_key;
180
- delete nextPayload.prompt_cache_retention;
181
- delete nextPayload.previous_response_id;
182
-
183
- normalizeReasoning(nextPayload, ctx.model?.id);
184
-
185
- return nextPayload;
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "abelworkflow",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "Install AbelWorkflow into ~/.agents and create Claude, Codex, and Pi links.",
5
5
  "type": "module",
6
6
  "scripts": {