@martian-engineering/lossless-claw 0.1.4 → 0.1.5
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/index.ts +93 -9
- package/package.json +1 -1
- package/src/summarize.ts +303 -4
- package/src/types.ts +12 -1
package/index.ts
CHANGED
|
@@ -49,6 +49,13 @@ type PluginEnvSnapshot = {
|
|
|
49
49
|
|
|
50
50
|
type ReadEnvFn = (key: string) => string | undefined;
|
|
51
51
|
|
|
52
|
+
type CompleteSimpleOptions = {
|
|
53
|
+
apiKey?: string;
|
|
54
|
+
maxTokens: number;
|
|
55
|
+
temperature?: number;
|
|
56
|
+
reasoning?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
52
59
|
/** Capture plugin env values once during initialization. */
|
|
53
60
|
function snapshotPluginEnv(env: NodeJS.ProcessEnv = process.env): PluginEnvSnapshot {
|
|
54
61
|
return {
|
|
@@ -130,13 +137,17 @@ type PiAiModule = {
|
|
|
130
137
|
contextWindow?: number;
|
|
131
138
|
maxTokens?: number;
|
|
132
139
|
},
|
|
133
|
-
request: {
|
|
140
|
+
request: {
|
|
141
|
+
systemPrompt?: string;
|
|
142
|
+
messages: Array<{ role: string; content: unknown; timestamp?: number }>;
|
|
143
|
+
},
|
|
134
144
|
options: {
|
|
135
145
|
apiKey?: string;
|
|
136
146
|
maxTokens: number;
|
|
137
147
|
temperature?: number;
|
|
148
|
+
reasoning?: string;
|
|
138
149
|
},
|
|
139
|
-
) => Promise<{ content?: Array<{ type: string; text?: string }> }>;
|
|
150
|
+
) => Promise<Record<string, unknown> & { content?: Array<{ type: string; text?: string }> }>;
|
|
140
151
|
getModel?: (provider: string, modelId: string) => unknown;
|
|
141
152
|
getModels?: (provider: string) => unknown[];
|
|
142
153
|
getEnvApiKey?: (provider: string) => string | undefined;
|
|
@@ -173,6 +184,39 @@ function inferApiFromProvider(provider: string): string {
|
|
|
173
184
|
return map[normalized] ?? "openai-responses";
|
|
174
185
|
}
|
|
175
186
|
|
|
187
|
+
/** Codex Responses rejects `temperature`; omit it for that API family. */
|
|
188
|
+
export function shouldOmitTemperatureForApi(api: string | undefined): boolean {
|
|
189
|
+
return (api ?? "").trim().toLowerCase() === "openai-codex-responses";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Build provider-aware options for pi-ai completeSimple. */
|
|
193
|
+
export function buildCompleteSimpleOptions(params: {
|
|
194
|
+
api: string | undefined;
|
|
195
|
+
apiKey: string | undefined;
|
|
196
|
+
maxTokens: number;
|
|
197
|
+
temperature: number | undefined;
|
|
198
|
+
reasoning: string | undefined;
|
|
199
|
+
}): CompleteSimpleOptions {
|
|
200
|
+
const options: CompleteSimpleOptions = {
|
|
201
|
+
apiKey: params.apiKey,
|
|
202
|
+
maxTokens: params.maxTokens,
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
if (
|
|
206
|
+
typeof params.temperature === "number" &&
|
|
207
|
+
Number.isFinite(params.temperature) &&
|
|
208
|
+
!shouldOmitTemperatureForApi(params.api)
|
|
209
|
+
) {
|
|
210
|
+
options.temperature = params.temperature;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (typeof params.reasoning === "string" && params.reasoning.trim()) {
|
|
214
|
+
options.reasoning = params.reasoning.trim();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return options;
|
|
218
|
+
}
|
|
219
|
+
|
|
176
220
|
/** Select provider-specific config values with case-insensitive provider keys. */
|
|
177
221
|
function findProviderConfigValue<T>(
|
|
178
222
|
map: Record<string, T> | undefined,
|
|
@@ -566,8 +610,10 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
566
610
|
agentDir,
|
|
567
611
|
runtimeConfig,
|
|
568
612
|
messages,
|
|
613
|
+
system,
|
|
569
614
|
maxTokens,
|
|
570
615
|
temperature,
|
|
616
|
+
reasoning,
|
|
571
617
|
}) => {
|
|
572
618
|
try {
|
|
573
619
|
const piAiModuleId = "@mariozechner/pi-ai";
|
|
@@ -644,24 +690,62 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
644
690
|
});
|
|
645
691
|
}
|
|
646
692
|
|
|
693
|
+
const completeOptions = buildCompleteSimpleOptions({
|
|
694
|
+
api: resolvedModel.api,
|
|
695
|
+
apiKey: resolvedApiKey,
|
|
696
|
+
maxTokens,
|
|
697
|
+
temperature,
|
|
698
|
+
reasoning,
|
|
699
|
+
});
|
|
700
|
+
|
|
647
701
|
const result = await mod.completeSimple(
|
|
648
702
|
resolvedModel,
|
|
649
703
|
{
|
|
704
|
+
...(typeof system === "string" && system.trim()
|
|
705
|
+
? { systemPrompt: system.trim() }
|
|
706
|
+
: {}),
|
|
650
707
|
messages: messages.map((message) => ({
|
|
651
708
|
role: message.role,
|
|
652
709
|
content: message.content,
|
|
653
710
|
timestamp: Date.now(),
|
|
654
711
|
})),
|
|
655
712
|
},
|
|
656
|
-
|
|
657
|
-
apiKey: resolvedApiKey,
|
|
658
|
-
maxTokens,
|
|
659
|
-
temperature,
|
|
660
|
-
},
|
|
713
|
+
completeOptions,
|
|
661
714
|
);
|
|
662
715
|
|
|
716
|
+
if (!isRecord(result)) {
|
|
717
|
+
return {
|
|
718
|
+
content: [],
|
|
719
|
+
request_provider: providerId,
|
|
720
|
+
request_model: modelId,
|
|
721
|
+
request_api: resolvedModel.api,
|
|
722
|
+
request_reasoning:
|
|
723
|
+
typeof reasoning === "string" && reasoning.trim() ? reasoning.trim() : "(none)",
|
|
724
|
+
request_has_system:
|
|
725
|
+
typeof system === "string" && system.trim().length > 0 ? "true" : "false",
|
|
726
|
+
request_temperature:
|
|
727
|
+
typeof completeOptions.temperature === "number"
|
|
728
|
+
? String(completeOptions.temperature)
|
|
729
|
+
: "(omitted)",
|
|
730
|
+
request_temperature_sent:
|
|
731
|
+
typeof completeOptions.temperature === "number" ? "true" : "false",
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
663
735
|
return {
|
|
664
|
-
|
|
736
|
+
...result,
|
|
737
|
+
content: Array.isArray(result.content) ? result.content : [],
|
|
738
|
+
request_provider: providerId,
|
|
739
|
+
request_model: modelId,
|
|
740
|
+
request_api: resolvedModel.api,
|
|
741
|
+
request_reasoning:
|
|
742
|
+
typeof reasoning === "string" && reasoning.trim() ? reasoning.trim() : "(none)",
|
|
743
|
+
request_has_system: typeof system === "string" && system.trim().length > 0 ? "true" : "false",
|
|
744
|
+
request_temperature:
|
|
745
|
+
typeof completeOptions.temperature === "number"
|
|
746
|
+
? String(completeOptions.temperature)
|
|
747
|
+
: "(omitted)",
|
|
748
|
+
request_temperature_sent: typeof completeOptions.temperature === "number" ? "true" : "false",
|
|
665
749
|
};
|
|
666
750
|
} catch (err) {
|
|
667
751
|
console.error(`[lcm] completeSimple error:`, err instanceof Error ? err.message : err);
|
|
@@ -715,8 +799,8 @@ function createLcmDependencies(api: OpenClawPluginApi): LcmDependencies {
|
|
|
715
799
|
}
|
|
716
800
|
|
|
717
801
|
const provider = (
|
|
718
|
-
providerHint?.trim() ||
|
|
719
802
|
envSnapshot.lcmSummaryProvider ||
|
|
803
|
+
providerHint?.trim() ||
|
|
720
804
|
envSnapshot.openclawProvider ||
|
|
721
805
|
"openai"
|
|
722
806
|
).trim();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@martian-engineering/lossless-claw",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Lossless Context Management plugin for OpenClaw — DAG-based conversation summarization with incremental compaction",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
package/src/summarize.ts
CHANGED
|
@@ -24,6 +24,14 @@ export type LcmSummarizerLegacyParams = {
|
|
|
24
24
|
type SummaryMode = "normal" | "aggressive";
|
|
25
25
|
|
|
26
26
|
const DEFAULT_CONDENSED_TARGET_TOKENS = 2000;
|
|
27
|
+
const LCM_SUMMARIZER_SYSTEM_PROMPT =
|
|
28
|
+
"You are a context-compaction summarization engine. Follow user instructions exactly and return plain text summary content only.";
|
|
29
|
+
const DIAGNOSTIC_MAX_DEPTH = 4;
|
|
30
|
+
const DIAGNOSTIC_MAX_ARRAY_ITEMS = 8;
|
|
31
|
+
const DIAGNOSTIC_MAX_OBJECT_KEYS = 16;
|
|
32
|
+
const DIAGNOSTIC_MAX_CHARS = 1200;
|
|
33
|
+
const DIAGNOSTIC_SENSITIVE_KEY_PATTERN =
|
|
34
|
+
/(api[-_]?key|authorization|token|secret|password|cookie|set-cookie|private[-_]?key|bearer)/i;
|
|
27
35
|
|
|
28
36
|
/** Normalize provider ids for stable config/profile lookup. */
|
|
29
37
|
function normalizeProviderId(provider: string): string {
|
|
@@ -193,6 +201,202 @@ function formatBlockTypes(blockTypes: string[]): string {
|
|
|
193
201
|
return blockTypes.join(",");
|
|
194
202
|
}
|
|
195
203
|
|
|
204
|
+
/** Truncate long diagnostic text values to keep logs bounded and readable. */
|
|
205
|
+
function truncateDiagnosticText(value: string, maxChars = DIAGNOSTIC_MAX_CHARS): string {
|
|
206
|
+
if (value.length <= maxChars) {
|
|
207
|
+
return value;
|
|
208
|
+
}
|
|
209
|
+
return `${value.slice(0, maxChars)}...[truncated:${value.length - maxChars} chars]`;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Build a JSON-safe, redacted, depth-limited clone for diagnostic logging. */
|
|
213
|
+
function sanitizeForDiagnostics(value: unknown, depth = 0): unknown {
|
|
214
|
+
if (depth >= DIAGNOSTIC_MAX_DEPTH) {
|
|
215
|
+
return "[max-depth]";
|
|
216
|
+
}
|
|
217
|
+
if (typeof value === "string") {
|
|
218
|
+
return truncateDiagnosticText(value);
|
|
219
|
+
}
|
|
220
|
+
if (
|
|
221
|
+
value === null ||
|
|
222
|
+
typeof value === "number" ||
|
|
223
|
+
typeof value === "boolean" ||
|
|
224
|
+
typeof value === "bigint"
|
|
225
|
+
) {
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
228
|
+
if (value === undefined) {
|
|
229
|
+
return "[undefined]";
|
|
230
|
+
}
|
|
231
|
+
if (typeof value === "function") {
|
|
232
|
+
return "[function]";
|
|
233
|
+
}
|
|
234
|
+
if (typeof value === "symbol") {
|
|
235
|
+
return "[symbol]";
|
|
236
|
+
}
|
|
237
|
+
if (Array.isArray(value)) {
|
|
238
|
+
const head = value
|
|
239
|
+
.slice(0, DIAGNOSTIC_MAX_ARRAY_ITEMS)
|
|
240
|
+
.map((entry) => sanitizeForDiagnostics(entry, depth + 1));
|
|
241
|
+
if (value.length > DIAGNOSTIC_MAX_ARRAY_ITEMS) {
|
|
242
|
+
head.push(`[+${value.length - DIAGNOSTIC_MAX_ARRAY_ITEMS} more items]`);
|
|
243
|
+
}
|
|
244
|
+
return head;
|
|
245
|
+
}
|
|
246
|
+
if (!isRecord(value)) {
|
|
247
|
+
return String(value);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const out: Record<string, unknown> = {};
|
|
251
|
+
const entries = Object.entries(value);
|
|
252
|
+
for (const [key, entry] of entries.slice(0, DIAGNOSTIC_MAX_OBJECT_KEYS)) {
|
|
253
|
+
out[key] = DIAGNOSTIC_SENSITIVE_KEY_PATTERN.test(key)
|
|
254
|
+
? "[redacted]"
|
|
255
|
+
: sanitizeForDiagnostics(entry, depth + 1);
|
|
256
|
+
}
|
|
257
|
+
if (entries.length > DIAGNOSTIC_MAX_OBJECT_KEYS) {
|
|
258
|
+
out.__truncated_keys__ = entries.length - DIAGNOSTIC_MAX_OBJECT_KEYS;
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Encode diagnostic payloads in a compact JSON string with safety guards. */
|
|
264
|
+
function formatDiagnosticPayload(value: unknown): string {
|
|
265
|
+
try {
|
|
266
|
+
const json = JSON.stringify(sanitizeForDiagnostics(value));
|
|
267
|
+
if (!json) {
|
|
268
|
+
return "\"\"";
|
|
269
|
+
}
|
|
270
|
+
return truncateDiagnosticText(json);
|
|
271
|
+
} catch {
|
|
272
|
+
return "\"[unserializable]\"";
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Extract safe diagnostic metadata from a provider response envelope.
|
|
278
|
+
*
|
|
279
|
+
* Picks common metadata fields (request id, model echo, usage counters) without
|
|
280
|
+
* leaking secrets like API keys or auth tokens. The result object from
|
|
281
|
+
* `deps.complete` is typed narrowly but real provider responses carry extra
|
|
282
|
+
* fields that are useful for debugging empty-summary incidents.
|
|
283
|
+
*/
|
|
284
|
+
function extractResponseDiagnostics(result: unknown): string {
|
|
285
|
+
if (!isRecord(result)) {
|
|
286
|
+
return "";
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const parts: string[] = [];
|
|
290
|
+
|
|
291
|
+
// Envelope-shape diagnostics for empty-block incidents.
|
|
292
|
+
const topLevelKeys = Object.keys(result).slice(0, 24);
|
|
293
|
+
if (topLevelKeys.length > 0) {
|
|
294
|
+
parts.push(`keys=${topLevelKeys.join(",")}`);
|
|
295
|
+
}
|
|
296
|
+
if ("content" in result) {
|
|
297
|
+
const contentVal = result.content;
|
|
298
|
+
if (Array.isArray(contentVal)) {
|
|
299
|
+
parts.push(`content_kind=array`);
|
|
300
|
+
parts.push(`content_len=${contentVal.length}`);
|
|
301
|
+
} else if (contentVal === null) {
|
|
302
|
+
parts.push(`content_kind=null`);
|
|
303
|
+
} else {
|
|
304
|
+
parts.push(`content_kind=${typeof contentVal}`);
|
|
305
|
+
}
|
|
306
|
+
parts.push(`content_preview=${formatDiagnosticPayload(contentVal)}`);
|
|
307
|
+
} else {
|
|
308
|
+
parts.push("content_kind=missing");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Preview common non-content payload envelopes used by provider SDKs.
|
|
312
|
+
const envelopePayload: Record<string, unknown> = {};
|
|
313
|
+
for (const key of ["summary", "output", "message", "response"]) {
|
|
314
|
+
if (key in result) {
|
|
315
|
+
envelopePayload[key] = result[key];
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (Object.keys(envelopePayload).length > 0) {
|
|
319
|
+
parts.push(`payload_preview=${formatDiagnosticPayload(envelopePayload)}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Request / response id — present in most provider envelopes.
|
|
323
|
+
for (const key of ["id", "request_id", "x-request-id"]) {
|
|
324
|
+
const val = result[key];
|
|
325
|
+
if (typeof val === "string" && val.trim()) {
|
|
326
|
+
parts.push(`${key}=${val.trim()}`);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Model echo — useful when the provider selects a different checkpoint.
|
|
331
|
+
if (typeof result.model === "string" && result.model.trim()) {
|
|
332
|
+
parts.push(`resp_model=${result.model.trim()}`);
|
|
333
|
+
}
|
|
334
|
+
if (typeof result.provider === "string" && result.provider.trim()) {
|
|
335
|
+
parts.push(`resp_provider=${result.provider.trim()}`);
|
|
336
|
+
}
|
|
337
|
+
for (const key of [
|
|
338
|
+
"request_provider",
|
|
339
|
+
"request_model",
|
|
340
|
+
"request_api",
|
|
341
|
+
"request_reasoning",
|
|
342
|
+
"request_has_system",
|
|
343
|
+
"request_temperature",
|
|
344
|
+
"request_temperature_sent",
|
|
345
|
+
]) {
|
|
346
|
+
const val = result[key];
|
|
347
|
+
if (typeof val === "string" && val.trim()) {
|
|
348
|
+
parts.push(`${key}=${val.trim()}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Usage counters — safe numeric diagnostics.
|
|
353
|
+
if (isRecord(result.usage)) {
|
|
354
|
+
const u = result.usage;
|
|
355
|
+
const tokens: string[] = [];
|
|
356
|
+
for (const k of [
|
|
357
|
+
"prompt_tokens",
|
|
358
|
+
"completion_tokens",
|
|
359
|
+
"total_tokens",
|
|
360
|
+
"input",
|
|
361
|
+
"output",
|
|
362
|
+
"cacheRead",
|
|
363
|
+
"cacheWrite",
|
|
364
|
+
]) {
|
|
365
|
+
if (typeof u[k] === "number") {
|
|
366
|
+
tokens.push(`${k}=${u[k]}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (tokens.length > 0) {
|
|
370
|
+
parts.push(tokens.join(","));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Finish reason — helps explain empty content.
|
|
375
|
+
const finishReason =
|
|
376
|
+
typeof result.finish_reason === "string"
|
|
377
|
+
? result.finish_reason
|
|
378
|
+
: typeof result.stopReason === "string"
|
|
379
|
+
? result.stopReason
|
|
380
|
+
: typeof result.stop_reason === "string"
|
|
381
|
+
? result.stop_reason
|
|
382
|
+
: undefined;
|
|
383
|
+
if (finishReason) {
|
|
384
|
+
parts.push(`finish=${finishReason}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Provider-level error payloads (most useful when finish=error and content is empty).
|
|
388
|
+
const errorMessage = result.errorMessage;
|
|
389
|
+
if (typeof errorMessage === "string" && errorMessage.trim()) {
|
|
390
|
+
parts.push(`error_message=${truncateDiagnosticText(errorMessage.trim(), 400)}`);
|
|
391
|
+
}
|
|
392
|
+
const errorPayload = result.error;
|
|
393
|
+
if (errorPayload !== undefined) {
|
|
394
|
+
parts.push(`error_preview=${formatDiagnosticPayload(errorPayload)}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return parts.join("; ");
|
|
398
|
+
}
|
|
399
|
+
|
|
196
400
|
/**
|
|
197
401
|
* Resolve a practical target token count for leaf and condensed summaries.
|
|
198
402
|
* Aggressive leaf mode intentionally aims lower so compaction converges faster.
|
|
@@ -522,6 +726,7 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
522
726
|
authProfileId,
|
|
523
727
|
agentDir,
|
|
524
728
|
runtimeConfig: params.legacyParams.config,
|
|
729
|
+
system: LCM_SUMMARIZER_SYSTEM_PROMPT,
|
|
525
730
|
messages: [
|
|
526
731
|
{
|
|
527
732
|
role: "user",
|
|
@@ -533,17 +738,111 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
533
738
|
});
|
|
534
739
|
|
|
535
740
|
const normalized = normalizeCompletionSummary(result.content);
|
|
536
|
-
|
|
741
|
+
let summary = normalized.summary;
|
|
742
|
+
let summarySource: "content" | "envelope" | "retry" | "fallback" = "content";
|
|
537
743
|
|
|
744
|
+
// --- Empty-summary hardening: envelope → retry → deterministic fallback ---
|
|
538
745
|
if (!summary) {
|
|
746
|
+
// Envelope-aware extraction: some providers place summary text in
|
|
747
|
+
// top-level response fields (output, message, response) rather than
|
|
748
|
+
// inside the content array. Re-run normalization against the full
|
|
749
|
+
// response envelope before spending an API call on a retry.
|
|
750
|
+
const envelopeNormalized = normalizeCompletionSummary(result);
|
|
751
|
+
if (envelopeNormalized.summary) {
|
|
752
|
+
summary = envelopeNormalized.summary;
|
|
753
|
+
summarySource = "envelope";
|
|
754
|
+
console.error(
|
|
755
|
+
`[lcm] recovered summary from response envelope; provider=${provider}; model=${model}; ` +
|
|
756
|
+
`block_types=${formatBlockTypes(envelopeNormalized.blockTypes)}; source=envelope`,
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (!summary) {
|
|
762
|
+
const responseDiag = extractResponseDiagnostics(result);
|
|
763
|
+
const diagParts = [
|
|
764
|
+
`[lcm] empty normalized summary on first attempt`,
|
|
765
|
+
`provider=${provider}`,
|
|
766
|
+
`model=${model}`,
|
|
767
|
+
`block_types=${formatBlockTypes(normalized.blockTypes)}`,
|
|
768
|
+
`response_blocks=${result.content.length}`,
|
|
769
|
+
];
|
|
770
|
+
if (responseDiag) {
|
|
771
|
+
diagParts.push(responseDiag);
|
|
772
|
+
}
|
|
773
|
+
console.error(`${diagParts.join("; ")}; retrying with conservative settings`);
|
|
774
|
+
|
|
775
|
+
// Single retry with conservative parameters: low temperature and low
|
|
776
|
+
// reasoning budget to coax a textual response from providers that
|
|
777
|
+
// sometimes return reasoning-only or empty blocks on the first pass.
|
|
778
|
+
try {
|
|
779
|
+
const retryResult = await params.deps.complete({
|
|
780
|
+
provider,
|
|
781
|
+
model,
|
|
782
|
+
apiKey,
|
|
783
|
+
providerApi,
|
|
784
|
+
authProfileId,
|
|
785
|
+
agentDir,
|
|
786
|
+
runtimeConfig: params.legacyParams.config,
|
|
787
|
+
system: LCM_SUMMARIZER_SYSTEM_PROMPT,
|
|
788
|
+
messages: [
|
|
789
|
+
{
|
|
790
|
+
role: "user",
|
|
791
|
+
content: prompt,
|
|
792
|
+
},
|
|
793
|
+
],
|
|
794
|
+
maxTokens: targetTokens,
|
|
795
|
+
temperature: 0.05,
|
|
796
|
+
reasoning: "low",
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
const retryNormalized = normalizeCompletionSummary(retryResult.content);
|
|
800
|
+
summary = retryNormalized.summary;
|
|
801
|
+
|
|
802
|
+
if (summary) {
|
|
803
|
+
summarySource = "retry";
|
|
804
|
+
console.error(
|
|
805
|
+
`[lcm] retry succeeded; provider=${provider}; model=${model}; ` +
|
|
806
|
+
`block_types=${formatBlockTypes(retryNormalized.blockTypes)}; source=retry`,
|
|
807
|
+
);
|
|
808
|
+
} else {
|
|
809
|
+
const retryDiag = extractResponseDiagnostics(retryResult);
|
|
810
|
+
const retryParts = [
|
|
811
|
+
`[lcm] retry also returned empty summary`,
|
|
812
|
+
`provider=${provider}`,
|
|
813
|
+
`model=${model}`,
|
|
814
|
+
`block_types=${formatBlockTypes(retryNormalized.blockTypes)}`,
|
|
815
|
+
`response_blocks=${retryResult.content.length}`,
|
|
816
|
+
];
|
|
817
|
+
if (retryDiag) {
|
|
818
|
+
retryParts.push(retryDiag);
|
|
819
|
+
}
|
|
820
|
+
console.error(`${retryParts.join("; ")}; falling back to truncation`);
|
|
821
|
+
}
|
|
822
|
+
} catch (retryErr) {
|
|
823
|
+
// Retry is best-effort; log and proceed to deterministic fallback.
|
|
824
|
+
console.error(
|
|
825
|
+
`[lcm] retry failed; provider=${provider} model=${model}; error=${
|
|
826
|
+
retryErr instanceof Error ? retryErr.message : String(retryErr)
|
|
827
|
+
}; falling back to truncation`,
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (!summary) {
|
|
833
|
+
summarySource = "fallback";
|
|
539
834
|
console.error(
|
|
540
|
-
`[lcm]
|
|
541
|
-
normalized.blockTypes,
|
|
542
|
-
)}; response_blocks=${result.content.length}; falling back to truncation`,
|
|
835
|
+
`[lcm] all extraction attempts exhausted; provider=${provider}; model=${model}; source=fallback`,
|
|
543
836
|
);
|
|
544
837
|
return buildDeterministicFallbackSummary(text, targetTokens);
|
|
545
838
|
}
|
|
546
839
|
|
|
840
|
+
if (summarySource !== "content") {
|
|
841
|
+
console.error(
|
|
842
|
+
`[lcm] summary resolved via non-content path; provider=${provider}; model=${model}; source=${summarySource}`,
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
|
|
547
846
|
return summary;
|
|
548
847
|
};
|
|
549
848
|
}
|
package/src/types.ts
CHANGED
|
@@ -11,6 +11,17 @@ import type { LcmConfig } from "./db/config.js";
|
|
|
11
11
|
* Minimal LLM completion interface needed by LCM for summarization.
|
|
12
12
|
* Matches the signature of completeSimple from @mariozechner/pi-ai.
|
|
13
13
|
*/
|
|
14
|
+
export type CompletionContentBlock = {
|
|
15
|
+
type: string;
|
|
16
|
+
text?: string;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type CompletionResult = {
|
|
21
|
+
content: CompletionContentBlock[];
|
|
22
|
+
[key: string]: unknown;
|
|
23
|
+
};
|
|
24
|
+
|
|
14
25
|
export type CompleteFn = (params: {
|
|
15
26
|
provider?: string;
|
|
16
27
|
model: string;
|
|
@@ -24,7 +35,7 @@ export type CompleteFn = (params: {
|
|
|
24
35
|
maxTokens: number;
|
|
25
36
|
temperature?: number;
|
|
26
37
|
reasoning?: string;
|
|
27
|
-
}) => Promise<
|
|
38
|
+
}) => Promise<CompletionResult>;
|
|
28
39
|
|
|
29
40
|
/**
|
|
30
41
|
* Gateway RPC call interface.
|