@martian-engineering/lossless-claw 0.5.2 → 0.5.3
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/README.md +18 -10
- package/docs/configuration.md +21 -0
- package/openclaw.plugin.json +39 -0
- package/package.json +1 -1
- package/src/assembler.ts +194 -3
- package/src/compaction.ts +203 -18
- package/src/db/config.ts +24 -3
- package/src/engine.ts +25 -6
- package/src/plugin/index.ts +111 -73
- package/src/store/summary-store.ts +80 -0
- package/src/summarize.ts +451 -209
- package/src/tools/lcm-expand-query-tool.ts +137 -34
- package/src/types.ts +1 -0
package/src/summarize.ts
CHANGED
|
@@ -20,8 +20,22 @@ export type LcmSummarizerLegacyParams = {
|
|
|
20
20
|
authProfileId?: unknown;
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
type SummaryResolutionCandidate = {
|
|
24
|
+
levelName: string;
|
|
25
|
+
modelRef: string;
|
|
26
|
+
providerHint?: string;
|
|
27
|
+
hasExplicitProvider: boolean;
|
|
28
|
+
useLegacyAuthProfile: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type ResolvedSummaryCandidate = SummaryResolutionCandidate & {
|
|
32
|
+
provider: string;
|
|
33
|
+
model: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
23
36
|
type SummaryMode = "normal" | "aggressive";
|
|
24
37
|
|
|
38
|
+
const DEFAULT_LEAF_TARGET_TOKENS = 2400;
|
|
25
39
|
const DEFAULT_CONDENSED_TARGET_TOKENS = 2000;
|
|
26
40
|
const LCM_SUMMARIZER_SYSTEM_PROMPT =
|
|
27
41
|
"You are a context-compaction summarization engine. Follow user instructions exactly and return plain text summary content only.";
|
|
@@ -188,6 +202,15 @@ function collectBlockTypes(value: unknown, out: Set<string>): void {
|
|
|
188
202
|
}
|
|
189
203
|
}
|
|
190
204
|
|
|
205
|
+
/** Treat provider reasoning/thinking payloads as diagnostics, not summary text. */
|
|
206
|
+
function isReasoningLikeType(type: unknown): boolean {
|
|
207
|
+
if (typeof type !== "string") {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
const normalized = type.trim().toLowerCase();
|
|
211
|
+
return normalized.includes("reasoning") || normalized.includes("thinking");
|
|
212
|
+
}
|
|
213
|
+
|
|
191
214
|
/** Collect text payloads from common provider response shapes. */
|
|
192
215
|
function collectTextLikeFields(value: unknown, out: string[]): void {
|
|
193
216
|
if (Array.isArray(value)) {
|
|
@@ -200,7 +223,11 @@ function collectTextLikeFields(value: unknown, out: string[]): void {
|
|
|
200
223
|
return;
|
|
201
224
|
}
|
|
202
225
|
|
|
203
|
-
|
|
226
|
+
if (isReasoningLikeType(value.type)) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for (const key of ["text", "output_text"]) {
|
|
204
231
|
appendTextValue(value[key], out);
|
|
205
232
|
}
|
|
206
233
|
for (const key of ["content", "summary", "output", "message", "response"]) {
|
|
@@ -517,6 +544,15 @@ function extractResponseDiagnostics(result: unknown): string {
|
|
|
517
544
|
if (typeof result.provider === "string" && result.provider.trim()) {
|
|
518
545
|
parts.push(`resp_provider=${result.provider.trim()}`);
|
|
519
546
|
}
|
|
547
|
+
if (typeof result.status === "string" && result.status.trim()) {
|
|
548
|
+
parts.push(`status=${result.status.trim()}`);
|
|
549
|
+
}
|
|
550
|
+
if (isRecord(result.incomplete_details) && typeof result.incomplete_details.reason === "string") {
|
|
551
|
+
const reason = result.incomplete_details.reason.trim();
|
|
552
|
+
if (reason) {
|
|
553
|
+
parts.push(`incomplete_reason=${reason}`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
520
556
|
for (const key of [
|
|
521
557
|
"request_provider",
|
|
522
558
|
"request_model",
|
|
@@ -580,6 +616,50 @@ function extractResponseDiagnostics(result: unknown): string {
|
|
|
580
616
|
return parts.join("; ");
|
|
581
617
|
}
|
|
582
618
|
|
|
619
|
+
/** Collect retry-worthy "incomplete" signals from Responses-style envelopes/items. */
|
|
620
|
+
function collectIncompleteResponseSignals(
|
|
621
|
+
value: unknown,
|
|
622
|
+
out: Set<string>,
|
|
623
|
+
label = "response",
|
|
624
|
+
depth = 0,
|
|
625
|
+
): void {
|
|
626
|
+
if (depth >= DIAGNOSTIC_MAX_DEPTH) {
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
if (Array.isArray(value)) {
|
|
630
|
+
value.slice(0, DIAGNOSTIC_MAX_ARRAY_ITEMS).forEach((entry, index) => {
|
|
631
|
+
collectIncompleteResponseSignals(entry, out, `${label}[${index}]`, depth + 1);
|
|
632
|
+
});
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (!isRecord(value)) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (typeof value.status === "string" && value.status.trim().toLowerCase() === "incomplete") {
|
|
640
|
+
out.add(`${label}.status=incomplete`);
|
|
641
|
+
}
|
|
642
|
+
if (isRecord(value.incomplete_details) && typeof value.incomplete_details.reason === "string") {
|
|
643
|
+
const reason = value.incomplete_details.reason.trim();
|
|
644
|
+
if (reason) {
|
|
645
|
+
out.add(`${label}.reason=${reason}`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
for (const key of ["content", "output", "message", "response", "items"] as const) {
|
|
650
|
+
if (key in value) {
|
|
651
|
+
collectIncompleteResponseSignals(value[key], out, `${label}.${key}`, depth + 1);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/** Extract retry-worthy incomplete-response diagnostics for provider envelopes/items. */
|
|
657
|
+
function extractIncompleteResponseSignals(value: unknown): string[] {
|
|
658
|
+
const signals = new Set<string>();
|
|
659
|
+
collectIncompleteResponseSignals(value, signals);
|
|
660
|
+
return [...signals].sort((a, b) => a.localeCompare(b));
|
|
661
|
+
}
|
|
662
|
+
|
|
583
663
|
/**
|
|
584
664
|
* Resolve a practical target token count for leaf and condensed summaries.
|
|
585
665
|
* Aggressive leaf mode intentionally aims lower so compaction converges faster.
|
|
@@ -588,6 +668,7 @@ function resolveTargetTokens(params: {
|
|
|
588
668
|
inputTokens: number;
|
|
589
669
|
mode: SummaryMode;
|
|
590
670
|
isCondensed: boolean;
|
|
671
|
+
leafTargetTokens: number;
|
|
591
672
|
condensedTargetTokens: number;
|
|
592
673
|
}): number {
|
|
593
674
|
if (params.isCondensed) {
|
|
@@ -595,10 +676,12 @@ function resolveTargetTokens(params: {
|
|
|
595
676
|
}
|
|
596
677
|
|
|
597
678
|
const { inputTokens, mode } = params;
|
|
679
|
+
const leafTargetTokens = Math.max(192, params.leafTargetTokens);
|
|
598
680
|
if (mode === "aggressive") {
|
|
599
|
-
|
|
681
|
+
const aggressiveCap = Math.max(96, Math.min(leafTargetTokens, Math.floor(leafTargetTokens * 0.55)));
|
|
682
|
+
return Math.max(96, Math.min(aggressiveCap, Math.floor(inputTokens * 0.2)));
|
|
600
683
|
}
|
|
601
|
-
return Math.max(192, Math.min(
|
|
684
|
+
return Math.max(192, Math.min(leafTargetTokens, Math.floor(inputTokens * 0.35)));
|
|
602
685
|
}
|
|
603
686
|
|
|
604
687
|
/**
|
|
@@ -815,30 +898,47 @@ function buildDeterministicFallbackSummary(text: string, targetTokens: number):
|
|
|
815
898
|
return `${trimmed.slice(0, maxChars)}\n[LCM fallback summary; truncated for context management]`;
|
|
816
899
|
}
|
|
817
900
|
|
|
818
|
-
/**
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
901
|
+
/** Normalize model refs from string or `{ primary }` config shapes. */
|
|
902
|
+
function readModelRef(value: unknown): string {
|
|
903
|
+
if (typeof value === "string") {
|
|
904
|
+
return value.trim();
|
|
905
|
+
}
|
|
906
|
+
const primary = (value as { primary?: unknown } | undefined)?.primary;
|
|
907
|
+
return typeof primary === "string" ? primary.trim() : "";
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** Avoid retrying the same resolved provider/model pair across fallback levels. */
|
|
911
|
+
function dedupeResolvedCandidates(
|
|
912
|
+
candidates: ResolvedSummaryCandidate[],
|
|
913
|
+
): ResolvedSummaryCandidate[] {
|
|
914
|
+
const seen = new Set<string>();
|
|
915
|
+
const ordered: ResolvedSummaryCandidate[] = [];
|
|
916
|
+
for (const candidate of candidates) {
|
|
917
|
+
const key = `${candidate.provider}\u0000${candidate.model}`;
|
|
918
|
+
if (seen.has(key)) {
|
|
919
|
+
continue;
|
|
832
920
|
}
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
}
|
|
921
|
+
seen.add(key);
|
|
922
|
+
ordered.push(candidate);
|
|
923
|
+
}
|
|
924
|
+
return ordered;
|
|
925
|
+
}
|
|
836
926
|
|
|
927
|
+
/** Resolve ordered summarizer candidates from env, plugin config, defaults, and session hints. */
|
|
928
|
+
function resolveSummaryCandidates(params: {
|
|
929
|
+
deps: LcmDependencies;
|
|
930
|
+
legacyParams: LcmSummarizerLegacyParams;
|
|
931
|
+
}): ResolvedSummaryCandidate[] {
|
|
932
|
+
const providerHint =
|
|
933
|
+
typeof params.legacyParams.provider === "string" ? params.legacyParams.provider.trim() : "";
|
|
934
|
+
const modelHint =
|
|
935
|
+
typeof params.legacyParams.model === "string" ? params.legacyParams.model.trim() : "";
|
|
837
936
|
const runtimeConfig =
|
|
838
937
|
params.legacyParams.config && typeof params.legacyParams.config === "object"
|
|
839
938
|
? (params.legacyParams.config as {
|
|
840
939
|
agents?: {
|
|
841
940
|
defaults?: {
|
|
941
|
+
model?: unknown;
|
|
842
942
|
compaction?: {
|
|
843
943
|
model?: unknown;
|
|
844
944
|
};
|
|
@@ -853,91 +953,121 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
853
953
|
};
|
|
854
954
|
})
|
|
855
955
|
: undefined;
|
|
856
|
-
|
|
857
956
|
const nestedPluginConfig = runtimeConfig?.plugins?.entries?.["lossless-claw"]?.config;
|
|
858
957
|
|
|
859
|
-
const
|
|
958
|
+
const resolutionCandidates: SummaryResolutionCandidate[] = [
|
|
860
959
|
{
|
|
861
960
|
levelName: "environment variables",
|
|
862
|
-
|
|
863
|
-
|
|
961
|
+
modelRef: process.env.LCM_SUMMARY_MODEL?.trim() ?? "",
|
|
962
|
+
providerHint:
|
|
963
|
+
process.env.LCM_SUMMARY_PROVIDER?.trim() ||
|
|
964
|
+
(providerHint || undefined),
|
|
965
|
+
hasExplicitProvider: Boolean(process.env.LCM_SUMMARY_PROVIDER?.trim()),
|
|
966
|
+
useLegacyAuthProfile: false,
|
|
864
967
|
},
|
|
865
968
|
{
|
|
866
969
|
levelName: "plugin config (lossless-claw)",
|
|
867
|
-
|
|
868
|
-
|
|
970
|
+
modelRef: readModelRef(nestedPluginConfig?.summaryModel),
|
|
971
|
+
providerHint:
|
|
972
|
+
(typeof nestedPluginConfig?.summaryProvider === "string"
|
|
973
|
+
? nestedPluginConfig.summaryProvider.trim()
|
|
974
|
+
: "") || (providerHint || undefined),
|
|
975
|
+
hasExplicitProvider: Boolean(
|
|
976
|
+
typeof nestedPluginConfig?.summaryProvider === "string" &&
|
|
977
|
+
nestedPluginConfig.summaryProvider.trim(),
|
|
978
|
+
),
|
|
979
|
+
useLegacyAuthProfile: false,
|
|
869
980
|
},
|
|
870
981
|
{
|
|
871
982
|
levelName: "OpenClaw agents.defaults.compaction.model",
|
|
872
|
-
|
|
873
|
-
|
|
983
|
+
modelRef: readModelRef(runtimeConfig?.agents?.defaults?.compaction?.model),
|
|
984
|
+
providerHint: undefined,
|
|
985
|
+
hasExplicitProvider: false,
|
|
986
|
+
useLegacyAuthProfile: false,
|
|
987
|
+
},
|
|
988
|
+
{
|
|
989
|
+
levelName: "OpenClaw agents.defaults.model",
|
|
990
|
+
modelRef: readModelRef(runtimeConfig?.agents?.defaults?.model),
|
|
991
|
+
providerHint: undefined,
|
|
992
|
+
hasExplicitProvider: false,
|
|
993
|
+
useLegacyAuthProfile: false,
|
|
994
|
+
},
|
|
995
|
+
{
|
|
996
|
+
levelName: "legacy runtime/session model",
|
|
997
|
+
modelRef: modelHint,
|
|
998
|
+
providerHint: providerHint || undefined,
|
|
999
|
+
hasExplicitProvider: Boolean(providerHint),
|
|
1000
|
+
useLegacyAuthProfile: true,
|
|
874
1001
|
},
|
|
875
1002
|
];
|
|
876
1003
|
|
|
877
|
-
|
|
878
|
-
for (const
|
|
879
|
-
if (!
|
|
880
|
-
|
|
881
|
-
resolvedSummary = { model: level.model, provider: undefined };
|
|
882
|
-
break;
|
|
1004
|
+
const resolvedCandidates: ResolvedSummaryCandidate[] = [];
|
|
1005
|
+
for (const candidate of resolutionCandidates) {
|
|
1006
|
+
if (!candidate.modelRef) {
|
|
1007
|
+
continue;
|
|
883
1008
|
}
|
|
884
|
-
if (
|
|
885
|
-
|
|
886
|
-
|
|
1009
|
+
if (!candidate.modelRef.includes("/") && !candidate.hasExplicitProvider) {
|
|
1010
|
+
params.deps.log.warn(
|
|
1011
|
+
`[lcm] summaryModel "${candidate.modelRef}" at "${candidate.levelName}" has no summaryProvider or provider prefix. Will attempt resolution without provider.`,
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
try {
|
|
1015
|
+
const resolved = params.deps.resolveModel(candidate.modelRef, candidate.providerHint);
|
|
1016
|
+
if (resolved.provider && resolved.model) {
|
|
1017
|
+
resolvedCandidates.push({
|
|
1018
|
+
...candidate,
|
|
1019
|
+
provider: resolved.provider,
|
|
1020
|
+
model: resolved.model,
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
} catch (err) {
|
|
1024
|
+
console.error(
|
|
1025
|
+
`[lcm] createLcmSummarize: resolveModel FAILED at ${candidate.levelName}:`,
|
|
1026
|
+
err instanceof Error ? err.message : err,
|
|
1027
|
+
);
|
|
887
1028
|
}
|
|
888
|
-
params.deps.log.warn(
|
|
889
|
-
`[lcm] summaryModel "${level.model}" at "${level.levelName}" has no summaryProvider or provider prefix. Will attempt resolution without provider.`
|
|
890
|
-
);
|
|
891
|
-
resolvedSummary = { model: level.model, provider: undefined };
|
|
892
|
-
break;
|
|
893
1029
|
}
|
|
894
1030
|
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
const modelHint =
|
|
898
|
-
typeof params.legacyParams.model === "string" ? params.legacyParams.model.trim() : "";
|
|
899
|
-
const modelRef = resolvedSummary?.model || modelHint || undefined;
|
|
900
|
-
|
|
901
|
-
const resolveProviderHint =
|
|
902
|
-
resolvedSummary !== undefined
|
|
903
|
-
? (
|
|
904
|
-
resolvedSummary.provider ||
|
|
905
|
-
(!resolvedSummary.model.includes("/") ? (providerHint || undefined) : undefined)
|
|
906
|
-
)
|
|
907
|
-
: (providerHint || undefined);
|
|
1031
|
+
return dedupeResolvedCandidates(resolvedCandidates);
|
|
1032
|
+
}
|
|
908
1033
|
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1034
|
+
/**
|
|
1035
|
+
* Builds a model-backed LCM summarize callback from runtime legacy params.
|
|
1036
|
+
*
|
|
1037
|
+
* Returns `undefined` when model/provider context is unavailable so callers can
|
|
1038
|
+
* choose a fallback summarizer.
|
|
1039
|
+
*/
|
|
1040
|
+
export async function createLcmSummarizeFromLegacyParams(params: {
|
|
1041
|
+
deps: LcmDependencies;
|
|
1042
|
+
legacyParams: LcmSummarizerLegacyParams;
|
|
1043
|
+
customInstructions?: string;
|
|
1044
|
+
}): Promise<{ fn: LcmSummarizeFn; model: string } | undefined> {
|
|
1045
|
+
const resolvedCandidates = resolveSummaryCandidates(params);
|
|
1046
|
+
if (resolvedCandidates.length === 0) {
|
|
1047
|
+
console.error("[lcm] createLcmSummarize: no summary model candidates resolved");
|
|
914
1048
|
return undefined;
|
|
915
1049
|
}
|
|
916
1050
|
|
|
917
|
-
const { provider, model } = resolved;
|
|
918
|
-
if (!provider || !model) {
|
|
919
|
-
console.error(`[lcm] createLcmSummarize: empty provider="${provider}" or model="${model}"`);
|
|
920
|
-
return undefined;
|
|
921
|
-
}
|
|
922
1051
|
const legacyAuthProfileId =
|
|
923
1052
|
typeof params.legacyParams.authProfileId === "string" &&
|
|
924
1053
|
params.legacyParams.authProfileId.trim()
|
|
925
1054
|
? params.legacyParams.authProfileId.trim()
|
|
926
1055
|
: undefined;
|
|
927
|
-
// When LCM selects a dedicated summarizer model/provider, do not leak the
|
|
928
|
-
// active session's auth profile into that separate credential lookup.
|
|
929
|
-
const authProfileId = resolvedSummary === undefined ? legacyAuthProfileId : undefined;
|
|
930
1056
|
const agentDir =
|
|
931
1057
|
typeof params.legacyParams.agentDir === "string" && params.legacyParams.agentDir.trim()
|
|
932
1058
|
? params.legacyParams.agentDir.trim()
|
|
933
1059
|
: undefined;
|
|
934
|
-
const providerApi = resolveProviderApiFromLegacyConfig(params.legacyParams.config, provider);
|
|
935
1060
|
|
|
936
1061
|
const condensedTargetTokens =
|
|
937
1062
|
Number.isFinite(params.deps.config.condensedTargetTokens) &&
|
|
938
1063
|
params.deps.config.condensedTargetTokens > 0
|
|
939
1064
|
? params.deps.config.condensedTargetTokens
|
|
940
1065
|
: DEFAULT_CONDENSED_TARGET_TOKENS;
|
|
1066
|
+
const leafTargetTokens =
|
|
1067
|
+
Number.isFinite(params.deps.config.leafTargetTokens) &&
|
|
1068
|
+
params.deps.config.leafTargetTokens > 0
|
|
1069
|
+
? params.deps.config.leafTargetTokens
|
|
1070
|
+
: DEFAULT_LEAF_TARGET_TOKENS;
|
|
941
1071
|
|
|
942
1072
|
const fn: LcmSummarizeFn = async (
|
|
943
1073
|
text: string,
|
|
@@ -950,15 +1080,11 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
950
1080
|
|
|
951
1081
|
const mode: SummaryMode = aggressive ? "aggressive" : "normal";
|
|
952
1082
|
const isCondensed = options?.isCondensed === true;
|
|
953
|
-
const apiKey = await params.deps.getApiKey(provider, model, {
|
|
954
|
-
profileId: authProfileId,
|
|
955
|
-
agentDir,
|
|
956
|
-
runtimeConfig: params.legacyParams.config,
|
|
957
|
-
});
|
|
958
1083
|
const targetTokens = resolveTargetTokens({
|
|
959
1084
|
inputTokens: estimateTokens(text),
|
|
960
1085
|
mode,
|
|
961
1086
|
isCondensed,
|
|
1087
|
+
leafTargetTokens,
|
|
962
1088
|
condensedTargetTokens,
|
|
963
1089
|
});
|
|
964
1090
|
const prompt = isCondensed
|
|
@@ -980,96 +1106,30 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
980
1106
|
customInstructions: params.customInstructions,
|
|
981
1107
|
});
|
|
982
1108
|
|
|
983
|
-
let
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1109
|
+
let lastAuthError: LcmProviderAuthError | undefined;
|
|
1110
|
+
|
|
1111
|
+
for (let index = 0; index < resolvedCandidates.length; index += 1) {
|
|
1112
|
+
const candidate = resolvedCandidates[index]!;
|
|
1113
|
+
const provider = candidate.provider;
|
|
1114
|
+
const model = candidate.model;
|
|
1115
|
+
const nextCandidate = index < resolvedCandidates.length - 1 ? resolvedCandidates[index + 1]! : undefined;
|
|
1116
|
+
const authProfileId = candidate.useLegacyAuthProfile ? legacyAuthProfileId : undefined;
|
|
1117
|
+
const providerApi = resolveProviderApiFromLegacyConfig(params.legacyParams.config, provider);
|
|
1118
|
+
const lookupOptions = {
|
|
1119
|
+
profileId: authProfileId,
|
|
991
1120
|
agentDir,
|
|
992
1121
|
runtimeConfig: params.legacyParams.config,
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
}), SUMMARIZER_TIMEOUT_MS, "initial");
|
|
1002
|
-
} catch (err) {
|
|
1003
|
-
const authFailure = extractProviderAuthFailure(err);
|
|
1004
|
-
if (authFailure) {
|
|
1005
|
-
const authError = new LcmProviderAuthError({ provider, model, failure: authFailure });
|
|
1006
|
-
console.warn(authError.message);
|
|
1007
|
-
throw authError;
|
|
1008
|
-
}
|
|
1009
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1010
|
-
const isTimeout = errMsg.includes("summarizer timeout");
|
|
1011
|
-
console.warn(
|
|
1012
|
-
`[lcm] summarizer ${isTimeout ? "timed out" : "failed"}; provider=${provider}; model=${model}; timeout=${SUMMARIZER_TIMEOUT_MS}ms; error=${errMsg}`,
|
|
1013
|
-
);
|
|
1014
|
-
if (err instanceof SummarizerTimeoutError) {
|
|
1015
|
-
console.error(
|
|
1016
|
-
`[lcm] summarizer timed out; provider=${provider}; model=${model}; source=fallback`,
|
|
1017
|
-
);
|
|
1018
|
-
return buildDeterministicFallbackSummary(text, targetTokens);
|
|
1019
|
-
}
|
|
1020
|
-
return "";
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
const authFailure = extractProviderAuthFailure(result);
|
|
1024
|
-
if (authFailure) {
|
|
1025
|
-
const authError = new LcmProviderAuthError({ provider, model, failure: authFailure });
|
|
1026
|
-
console.warn(authError.message);
|
|
1027
|
-
throw authError;
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
const normalized = normalizeCompletionSummary(result.content);
|
|
1031
|
-
let summary = normalized.summary;
|
|
1032
|
-
let summarySource: "content" | "envelope" | "retry" | "fallback" = "content";
|
|
1033
|
-
|
|
1034
|
-
// --- Empty-summary hardening: envelope → retry → deterministic fallback ---
|
|
1035
|
-
if (!summary) {
|
|
1036
|
-
// Envelope-aware extraction: some providers place summary text in
|
|
1037
|
-
// top-level response fields (output, message, response) rather than
|
|
1038
|
-
// inside the content array. Re-run normalization against the full
|
|
1039
|
-
// response envelope before spending an API call on a retry.
|
|
1040
|
-
const envelopeNormalized = normalizeCompletionSummary(result);
|
|
1041
|
-
if (envelopeNormalized.summary) {
|
|
1042
|
-
summary = envelopeNormalized.summary;
|
|
1043
|
-
summarySource = "envelope";
|
|
1044
|
-
console.error(
|
|
1045
|
-
`[lcm] recovered summary from response envelope; provider=${provider}; model=${model}; ` +
|
|
1046
|
-
`block_types=${formatBlockTypes(envelopeNormalized.blockTypes)}; source=envelope`,
|
|
1047
|
-
);
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
if (!summary) {
|
|
1052
|
-
const responseDiag = extractResponseDiagnostics(result);
|
|
1053
|
-
const diagParts = [
|
|
1054
|
-
`[lcm] empty normalized summary on first attempt`,
|
|
1055
|
-
`provider=${provider}`,
|
|
1056
|
-
`model=${model}`,
|
|
1057
|
-
`block_types=${formatBlockTypes(normalized.blockTypes)}`,
|
|
1058
|
-
`response_blocks=${result.content.length}`,
|
|
1059
|
-
];
|
|
1060
|
-
if (responseDiag) {
|
|
1061
|
-
diagParts.push(responseDiag);
|
|
1062
|
-
}
|
|
1063
|
-
console.error(`${diagParts.join("; ")}; retrying with conservative settings`);
|
|
1064
|
-
|
|
1065
|
-
// Single retry with conservative parameters: low temperature and low
|
|
1066
|
-
// reasoning budget to coax a textual response from providers that
|
|
1067
|
-
// sometimes return reasoning-only or empty blocks on the first pass.
|
|
1068
|
-
try {
|
|
1069
|
-
const retryResult = await withTimeout(params.deps.complete({
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
const runSummarizerCall = async (
|
|
1125
|
+
requestApiKey: string | undefined,
|
|
1126
|
+
label: string,
|
|
1127
|
+
reasoning?: string,
|
|
1128
|
+
) =>
|
|
1129
|
+
withTimeout(params.deps.complete({
|
|
1070
1130
|
provider,
|
|
1071
1131
|
model,
|
|
1072
|
-
apiKey,
|
|
1132
|
+
apiKey: requestApiKey,
|
|
1073
1133
|
providerApi,
|
|
1074
1134
|
authProfileId,
|
|
1075
1135
|
agentDir,
|
|
@@ -1082,68 +1142,250 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
1082
1142
|
},
|
|
1083
1143
|
],
|
|
1084
1144
|
maxTokens: targetTokens,
|
|
1085
|
-
reasoning:
|
|
1086
|
-
}), SUMMARIZER_TIMEOUT_MS,
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1145
|
+
...(reasoning ? { reasoning } : {}),
|
|
1146
|
+
}), SUMMARIZER_TIMEOUT_MS, label);
|
|
1147
|
+
|
|
1148
|
+
const retryWithoutModelAuth = async (
|
|
1149
|
+
failure: ProviderAuthFailure,
|
|
1150
|
+
reasoning?: string,
|
|
1151
|
+
): Promise<Awaited<ReturnType<typeof params.deps.complete>>> => {
|
|
1152
|
+
const initialAuthError = new LcmProviderAuthError({ provider, model, failure });
|
|
1153
|
+
console.warn(initialAuthError.message);
|
|
1154
|
+
console.warn(
|
|
1155
|
+
`[lcm] summarizer auth retry: retrying ${provider}/${model} without runtime.modelAuth credentials.`,
|
|
1156
|
+
);
|
|
1092
1157
|
|
|
1093
|
-
const
|
|
1094
|
-
|
|
1158
|
+
const directApiKey = await params.deps.getApiKey(provider, model, {
|
|
1159
|
+
...lookupOptions,
|
|
1160
|
+
skipModelAuth: true,
|
|
1161
|
+
});
|
|
1162
|
+
if (!directApiKey) {
|
|
1163
|
+
console.warn(
|
|
1164
|
+
`[lcm] summarizer auth retry unavailable: no direct credentials found for ${provider}/${model}.`,
|
|
1165
|
+
);
|
|
1166
|
+
throw initialAuthError;
|
|
1167
|
+
}
|
|
1095
1168
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1169
|
+
try {
|
|
1170
|
+
const directResult = await runSummarizerCall(directApiKey, "auth-retry", reasoning);
|
|
1171
|
+
const directFailure = extractProviderAuthFailure(directResult);
|
|
1172
|
+
if (directFailure) {
|
|
1173
|
+
const retryAuthError = new LcmProviderAuthError({
|
|
1174
|
+
provider,
|
|
1175
|
+
model,
|
|
1176
|
+
failure: directFailure,
|
|
1177
|
+
});
|
|
1178
|
+
console.warn(retryAuthError.message);
|
|
1179
|
+
throw retryAuthError;
|
|
1180
|
+
}
|
|
1181
|
+
console.warn(
|
|
1182
|
+
`[lcm] summarizer auth retry succeeded; provider=${provider}; model=${model}; source=direct-credentials`,
|
|
1101
1183
|
);
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
`provider=${provider}`,
|
|
1107
|
-
`model=${model}`,
|
|
1108
|
-
`block_types=${formatBlockTypes(retryNormalized.blockTypes)}`,
|
|
1109
|
-
`response_blocks=${retryResult.content.length}`,
|
|
1110
|
-
];
|
|
1111
|
-
if (retryDiag) {
|
|
1112
|
-
retryParts.push(retryDiag);
|
|
1184
|
+
return directResult;
|
|
1185
|
+
} catch (directErr) {
|
|
1186
|
+
if (directErr instanceof LcmProviderAuthError) {
|
|
1187
|
+
throw directErr;
|
|
1113
1188
|
}
|
|
1114
|
-
|
|
1189
|
+
const directFailure = extractProviderAuthFailure(directErr);
|
|
1190
|
+
if (directFailure) {
|
|
1191
|
+
const retryAuthError = new LcmProviderAuthError({
|
|
1192
|
+
provider,
|
|
1193
|
+
model,
|
|
1194
|
+
failure: directFailure,
|
|
1195
|
+
});
|
|
1196
|
+
console.warn(retryAuthError.message);
|
|
1197
|
+
throw retryAuthError;
|
|
1198
|
+
}
|
|
1199
|
+
throw directErr;
|
|
1115
1200
|
}
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
const attemptSummarizerCall = async (
|
|
1204
|
+
label: string,
|
|
1205
|
+
reasoning?: string,
|
|
1206
|
+
): Promise<Awaited<ReturnType<typeof params.deps.complete>>> => {
|
|
1207
|
+
const apiKey = await params.deps.getApiKey(provider, model, lookupOptions);
|
|
1208
|
+
try {
|
|
1209
|
+
const result = await runSummarizerCall(apiKey, label, reasoning);
|
|
1210
|
+
const authFailure = extractProviderAuthFailure(result);
|
|
1211
|
+
if (!authFailure) {
|
|
1212
|
+
return result;
|
|
1213
|
+
}
|
|
1214
|
+
return retryWithoutModelAuth(authFailure, reasoning);
|
|
1215
|
+
} catch (err) {
|
|
1216
|
+
const authFailure = extractProviderAuthFailure(err);
|
|
1217
|
+
if (!authFailure) {
|
|
1218
|
+
throw err;
|
|
1219
|
+
}
|
|
1220
|
+
return retryWithoutModelAuth(authFailure, reasoning);
|
|
1121
1221
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
let result: Awaited<ReturnType<typeof params.deps.complete>>;
|
|
1225
|
+
try {
|
|
1226
|
+
result = await attemptSummarizerCall("initial");
|
|
1227
|
+
} catch (err) {
|
|
1228
|
+
if (err instanceof LcmProviderAuthError) {
|
|
1229
|
+
lastAuthError = err;
|
|
1230
|
+
if (nextCandidate) {
|
|
1231
|
+
console.warn(
|
|
1232
|
+
`[lcm] summarizer auth fallback: retrying with ${nextCandidate.provider}/${nextCandidate.model} after ${provider}/${model} failed auth.`,
|
|
1233
|
+
);
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
throw lastAuthError;
|
|
1237
|
+
}
|
|
1238
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1239
|
+
const isTimeout = errMsg.includes("summarizer timeout");
|
|
1125
1240
|
console.warn(
|
|
1126
|
-
`[lcm]
|
|
1241
|
+
`[lcm] summarizer ${isTimeout ? "timed out" : "failed"}; provider=${provider}; model=${model}; timeout=${SUMMARIZER_TIMEOUT_MS}ms; error=${errMsg}`,
|
|
1127
1242
|
);
|
|
1243
|
+
if (nextCandidate) {
|
|
1244
|
+
console.warn(
|
|
1245
|
+
`[lcm] summarizer candidate fallback: retrying with ${nextCandidate.provider}/${nextCandidate.model} after ${provider}/${model} ${isTimeout ? "timed out" : "failed"}.`,
|
|
1246
|
+
);
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
if (err instanceof SummarizerTimeoutError) {
|
|
1250
|
+
console.error(
|
|
1251
|
+
`[lcm] summarizer timed out; provider=${provider}; model=${model}; source=fallback`,
|
|
1252
|
+
);
|
|
1253
|
+
return buildDeterministicFallbackSummary(text, targetTokens);
|
|
1254
|
+
}
|
|
1255
|
+
return "";
|
|
1128
1256
|
}
|
|
1129
|
-
}
|
|
1130
1257
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1258
|
+
const normalized = normalizeCompletionSummary(result.content);
|
|
1259
|
+
let summary = normalized.summary;
|
|
1260
|
+
let summarySource: "content" | "envelope" | "retry" | "fallback" = "content";
|
|
1261
|
+
|
|
1262
|
+
// --- Empty-summary hardening: envelope → retry → deterministic fallback ---
|
|
1263
|
+
if (!summary) {
|
|
1264
|
+
// Envelope-aware extraction: some providers place summary text in
|
|
1265
|
+
// top-level response fields (output, message, response) rather than
|
|
1266
|
+
// inside the content array. Re-run normalization against the full
|
|
1267
|
+
// response envelope before spending an API call on a retry.
|
|
1268
|
+
const envelopeNormalized = normalizeCompletionSummary(result);
|
|
1269
|
+
if (envelopeNormalized.summary) {
|
|
1270
|
+
summary = envelopeNormalized.summary;
|
|
1271
|
+
summarySource = "envelope";
|
|
1272
|
+
console.error(
|
|
1273
|
+
`[lcm] recovered summary from response envelope; provider=${provider}; model=${model}; ` +
|
|
1274
|
+
`block_types=${formatBlockTypes(envelopeNormalized.blockTypes)}; source=envelope`,
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1138
1278
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1279
|
+
const incompleteSignals = extractIncompleteResponseSignals(result);
|
|
1280
|
+
const initialSummary = summary;
|
|
1281
|
+
const shouldRetryIncompleteSummary = summary.length > 0 && incompleteSignals.length > 0;
|
|
1282
|
+
|
|
1283
|
+
if (!summary || shouldRetryIncompleteSummary) {
|
|
1284
|
+
const responseDiag = extractResponseDiagnostics(result);
|
|
1285
|
+
const diagParts = [
|
|
1286
|
+
shouldRetryIncompleteSummary
|
|
1287
|
+
? `[lcm] incomplete summary response on first attempt`
|
|
1288
|
+
: `[lcm] empty normalized summary on first attempt`,
|
|
1289
|
+
`provider=${provider}`,
|
|
1290
|
+
`model=${model}`,
|
|
1291
|
+
`block_types=${formatBlockTypes(normalized.blockTypes)}`,
|
|
1292
|
+
`response_blocks=${result.content.length}`,
|
|
1293
|
+
];
|
|
1294
|
+
if (incompleteSignals.length > 0) {
|
|
1295
|
+
diagParts.push(`incomplete=${incompleteSignals.join(",")}`);
|
|
1296
|
+
}
|
|
1297
|
+
if (responseDiag) {
|
|
1298
|
+
diagParts.push(responseDiag);
|
|
1299
|
+
}
|
|
1300
|
+
console.error(`${diagParts.join("; ")}; retrying with conservative settings`);
|
|
1301
|
+
|
|
1302
|
+
// Single retry with conservative parameters: low temperature and low
|
|
1303
|
+
// reasoning budget to coax a textual response from providers that
|
|
1304
|
+
// sometimes return reasoning-only or empty blocks on the first pass.
|
|
1305
|
+
try {
|
|
1306
|
+
const retryResult = await attemptSummarizerCall("retry", "low");
|
|
1307
|
+
const retryNormalized = normalizeCompletionSummary(retryResult.content);
|
|
1308
|
+
const retryEnvelopeNormalized = retryNormalized.summary
|
|
1309
|
+
? retryNormalized
|
|
1310
|
+
: normalizeCompletionSummary(retryResult);
|
|
1311
|
+
summary = retryEnvelopeNormalized.summary;
|
|
1312
|
+
|
|
1313
|
+
if (summary) {
|
|
1314
|
+
summarySource = "retry";
|
|
1315
|
+
console.error(
|
|
1316
|
+
`[lcm] retry succeeded; provider=${provider}; model=${model}; ` +
|
|
1317
|
+
`block_types=${formatBlockTypes(retryEnvelopeNormalized.blockTypes)}; source=retry`,
|
|
1318
|
+
);
|
|
1319
|
+
} else {
|
|
1320
|
+
const retryDiag = extractResponseDiagnostics(retryResult);
|
|
1321
|
+
const retryParts = [
|
|
1322
|
+
`[lcm] retry also returned empty summary`,
|
|
1323
|
+
`provider=${provider}`,
|
|
1324
|
+
`model=${model}`,
|
|
1325
|
+
`block_types=${formatBlockTypes(retryEnvelopeNormalized.blockTypes)}`,
|
|
1326
|
+
`response_blocks=${retryResult.content.length}`,
|
|
1327
|
+
];
|
|
1328
|
+
if (retryDiag) {
|
|
1329
|
+
retryParts.push(retryDiag);
|
|
1330
|
+
}
|
|
1331
|
+
if (nextCandidate) {
|
|
1332
|
+
console.warn(
|
|
1333
|
+
`${retryParts.join("; ")}; retrying with ${nextCandidate.provider}/${nextCandidate.model}`,
|
|
1334
|
+
);
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
console.error(`${retryParts.join("; ")}; falling back to truncation`);
|
|
1338
|
+
summary = initialSummary;
|
|
1339
|
+
}
|
|
1340
|
+
} catch (retryErr) {
|
|
1341
|
+
if (retryErr instanceof LcmProviderAuthError) {
|
|
1342
|
+
lastAuthError = retryErr;
|
|
1343
|
+
if (nextCandidate) {
|
|
1344
|
+
console.warn(
|
|
1345
|
+
`[lcm] summarizer auth fallback: retrying with ${nextCandidate.provider}/${nextCandidate.model} after ${provider}/${model} failed auth.`,
|
|
1346
|
+
);
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
throw lastAuthError;
|
|
1350
|
+
}
|
|
1351
|
+
// Retry is best-effort; log and proceed to deterministic fallback.
|
|
1352
|
+
const retryErrMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
1353
|
+
const isRetryTimeout = retryErrMsg.includes("summarizer timeout");
|
|
1354
|
+
if (nextCandidate) {
|
|
1355
|
+
console.warn(
|
|
1356
|
+
`[lcm] retry ${isRetryTimeout ? "timed out" : "failed"}; provider=${provider}; model=${model}; timeout=${SUMMARIZER_TIMEOUT_MS}ms; error=${retryErrMsg}; retrying with ${nextCandidate.provider}/${nextCandidate.model}`,
|
|
1357
|
+
);
|
|
1358
|
+
continue;
|
|
1359
|
+
}
|
|
1360
|
+
console.warn(
|
|
1361
|
+
`[lcm] retry ${isRetryTimeout ? "timed out" : "failed"}; provider=${provider}; model=${model}; timeout=${SUMMARIZER_TIMEOUT_MS}ms; error=${retryErrMsg}; falling back to truncation`,
|
|
1362
|
+
);
|
|
1363
|
+
summary = initialSummary;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if (!summary) {
|
|
1368
|
+
summarySource = "fallback";
|
|
1369
|
+
console.error(
|
|
1370
|
+
`[lcm] all extraction attempts exhausted; provider=${provider}; model=${model}; source=fallback`,
|
|
1371
|
+
);
|
|
1372
|
+
return buildDeterministicFallbackSummary(text, targetTokens);
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
if (summarySource !== "content") {
|
|
1376
|
+
console.error(
|
|
1377
|
+
`[lcm] summary resolved via non-content path; provider=${provider}; model=${model}; source=${summarySource}`,
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
return summary;
|
|
1143
1382
|
}
|
|
1144
1383
|
|
|
1145
|
-
|
|
1384
|
+
if (lastAuthError) {
|
|
1385
|
+
throw lastAuthError;
|
|
1386
|
+
}
|
|
1387
|
+
return "";
|
|
1146
1388
|
};
|
|
1147
1389
|
|
|
1148
|
-
return { fn, model };
|
|
1390
|
+
return { fn, model: resolvedCandidates[0]!.model };
|
|
1149
1391
|
}
|