@martian-engineering/lossless-claw 0.5.1 → 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 +20 -11
- package/docs/configuration.md +22 -0
- package/docs/tui.md +10 -1
- package/openclaw.plugin.json +39 -0
- package/package.json +1 -1
- package/src/assembler.ts +194 -3
- package/src/compaction.ts +231 -25
- package/src/db/config.ts +24 -3
- package/src/engine.ts +35 -8
- package/src/plugin/index.ts +113 -73
- package/src/store/summary-store.ts +80 -0
- package/src/summarize.ts +473 -209
- package/src/tools/lcm-expand-query-tool.ts +339 -144
- 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.";
|
|
@@ -42,6 +56,28 @@ type ProviderAuthFailure = {
|
|
|
42
56
|
missingModelRequestScope: boolean;
|
|
43
57
|
};
|
|
44
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Signals that the summarizer hit a provider-auth failure and callers should
|
|
61
|
+
* avoid treating the result like an empty summary.
|
|
62
|
+
*/
|
|
63
|
+
export class LcmProviderAuthError extends Error {
|
|
64
|
+
readonly provider: string;
|
|
65
|
+
readonly model: string;
|
|
66
|
+
readonly failure: ProviderAuthFailure;
|
|
67
|
+
|
|
68
|
+
constructor(params: {
|
|
69
|
+
provider: string;
|
|
70
|
+
model: string;
|
|
71
|
+
failure: ProviderAuthFailure;
|
|
72
|
+
}) {
|
|
73
|
+
super(buildProviderAuthWarning(params));
|
|
74
|
+
this.name = "LcmProviderAuthError";
|
|
75
|
+
this.provider = params.provider;
|
|
76
|
+
this.model = params.model;
|
|
77
|
+
this.failure = params.failure;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
45
81
|
/**
|
|
46
82
|
* Default timeout for a single summarizer LLM call. Long enough for large
|
|
47
83
|
* context windows on slower providers, short enough to prevent the gateway
|
|
@@ -166,6 +202,15 @@ function collectBlockTypes(value: unknown, out: Set<string>): void {
|
|
|
166
202
|
}
|
|
167
203
|
}
|
|
168
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
|
+
|
|
169
214
|
/** Collect text payloads from common provider response shapes. */
|
|
170
215
|
function collectTextLikeFields(value: unknown, out: string[]): void {
|
|
171
216
|
if (Array.isArray(value)) {
|
|
@@ -178,7 +223,11 @@ function collectTextLikeFields(value: unknown, out: string[]): void {
|
|
|
178
223
|
return;
|
|
179
224
|
}
|
|
180
225
|
|
|
181
|
-
|
|
226
|
+
if (isReasoningLikeType(value.type)) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
for (const key of ["text", "output_text"]) {
|
|
182
231
|
appendTextValue(value[key], out);
|
|
183
232
|
}
|
|
184
233
|
for (const key of ["content", "summary", "output", "message", "response"]) {
|
|
@@ -495,6 +544,15 @@ function extractResponseDiagnostics(result: unknown): string {
|
|
|
495
544
|
if (typeof result.provider === "string" && result.provider.trim()) {
|
|
496
545
|
parts.push(`resp_provider=${result.provider.trim()}`);
|
|
497
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
|
+
}
|
|
498
556
|
for (const key of [
|
|
499
557
|
"request_provider",
|
|
500
558
|
"request_model",
|
|
@@ -558,6 +616,50 @@ function extractResponseDiagnostics(result: unknown): string {
|
|
|
558
616
|
return parts.join("; ");
|
|
559
617
|
}
|
|
560
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
|
+
|
|
561
663
|
/**
|
|
562
664
|
* Resolve a practical target token count for leaf and condensed summaries.
|
|
563
665
|
* Aggressive leaf mode intentionally aims lower so compaction converges faster.
|
|
@@ -566,6 +668,7 @@ function resolveTargetTokens(params: {
|
|
|
566
668
|
inputTokens: number;
|
|
567
669
|
mode: SummaryMode;
|
|
568
670
|
isCondensed: boolean;
|
|
671
|
+
leafTargetTokens: number;
|
|
569
672
|
condensedTargetTokens: number;
|
|
570
673
|
}): number {
|
|
571
674
|
if (params.isCondensed) {
|
|
@@ -573,10 +676,12 @@ function resolveTargetTokens(params: {
|
|
|
573
676
|
}
|
|
574
677
|
|
|
575
678
|
const { inputTokens, mode } = params;
|
|
679
|
+
const leafTargetTokens = Math.max(192, params.leafTargetTokens);
|
|
576
680
|
if (mode === "aggressive") {
|
|
577
|
-
|
|
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)));
|
|
578
683
|
}
|
|
579
|
-
return Math.max(192, Math.min(
|
|
684
|
+
return Math.max(192, Math.min(leafTargetTokens, Math.floor(inputTokens * 0.35)));
|
|
580
685
|
}
|
|
581
686
|
|
|
582
687
|
/**
|
|
@@ -793,30 +898,47 @@ function buildDeterministicFallbackSummary(text: string, targetTokens: number):
|
|
|
793
898
|
return `${trimmed.slice(0, maxChars)}\n[LCM fallback summary; truncated for context management]`;
|
|
794
899
|
}
|
|
795
900
|
|
|
796
|
-
/**
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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;
|
|
810
920
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
}
|
|
921
|
+
seen.add(key);
|
|
922
|
+
ordered.push(candidate);
|
|
923
|
+
}
|
|
924
|
+
return ordered;
|
|
925
|
+
}
|
|
814
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() : "";
|
|
815
936
|
const runtimeConfig =
|
|
816
937
|
params.legacyParams.config && typeof params.legacyParams.config === "object"
|
|
817
938
|
? (params.legacyParams.config as {
|
|
818
939
|
agents?: {
|
|
819
940
|
defaults?: {
|
|
941
|
+
model?: unknown;
|
|
820
942
|
compaction?: {
|
|
821
943
|
model?: unknown;
|
|
822
944
|
};
|
|
@@ -831,91 +953,121 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
831
953
|
};
|
|
832
954
|
})
|
|
833
955
|
: undefined;
|
|
834
|
-
|
|
835
956
|
const nestedPluginConfig = runtimeConfig?.plugins?.entries?.["lossless-claw"]?.config;
|
|
836
957
|
|
|
837
|
-
const
|
|
958
|
+
const resolutionCandidates: SummaryResolutionCandidate[] = [
|
|
838
959
|
{
|
|
839
960
|
levelName: "environment variables",
|
|
840
|
-
|
|
841
|
-
|
|
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,
|
|
842
967
|
},
|
|
843
968
|
{
|
|
844
969
|
levelName: "plugin config (lossless-claw)",
|
|
845
|
-
|
|
846
|
-
|
|
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,
|
|
847
980
|
},
|
|
848
981
|
{
|
|
849
982
|
levelName: "OpenClaw agents.defaults.compaction.model",
|
|
850
|
-
|
|
851
|
-
|
|
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,
|
|
852
1001
|
},
|
|
853
1002
|
];
|
|
854
1003
|
|
|
855
|
-
|
|
856
|
-
for (const
|
|
857
|
-
if (!
|
|
858
|
-
|
|
859
|
-
resolvedSummary = { model: level.model, provider: undefined };
|
|
860
|
-
break;
|
|
1004
|
+
const resolvedCandidates: ResolvedSummaryCandidate[] = [];
|
|
1005
|
+
for (const candidate of resolutionCandidates) {
|
|
1006
|
+
if (!candidate.modelRef) {
|
|
1007
|
+
continue;
|
|
861
1008
|
}
|
|
862
|
-
if (
|
|
863
|
-
|
|
864
|
-
|
|
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
|
+
);
|
|
865
1028
|
}
|
|
866
|
-
params.deps.log.warn(
|
|
867
|
-
`[lcm] summaryModel "${level.model}" at "${level.levelName}" has no summaryProvider or provider prefix. Will attempt resolution without provider.`
|
|
868
|
-
);
|
|
869
|
-
resolvedSummary = { model: level.model, provider: undefined };
|
|
870
|
-
break;
|
|
871
1029
|
}
|
|
872
1030
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
const modelHint =
|
|
876
|
-
typeof params.legacyParams.model === "string" ? params.legacyParams.model.trim() : "";
|
|
877
|
-
const modelRef = resolvedSummary?.model || modelHint || undefined;
|
|
878
|
-
|
|
879
|
-
const resolveProviderHint =
|
|
880
|
-
resolvedSummary !== undefined
|
|
881
|
-
? (
|
|
882
|
-
resolvedSummary.provider ||
|
|
883
|
-
(!resolvedSummary.model.includes("/") ? (providerHint || undefined) : undefined)
|
|
884
|
-
)
|
|
885
|
-
: (providerHint || undefined);
|
|
1031
|
+
return dedupeResolvedCandidates(resolvedCandidates);
|
|
1032
|
+
}
|
|
886
1033
|
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
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");
|
|
892
1048
|
return undefined;
|
|
893
1049
|
}
|
|
894
1050
|
|
|
895
|
-
const { provider, model } = resolved;
|
|
896
|
-
if (!provider || !model) {
|
|
897
|
-
console.error(`[lcm] createLcmSummarize: empty provider="${provider}" or model="${model}"`);
|
|
898
|
-
return undefined;
|
|
899
|
-
}
|
|
900
1051
|
const legacyAuthProfileId =
|
|
901
1052
|
typeof params.legacyParams.authProfileId === "string" &&
|
|
902
1053
|
params.legacyParams.authProfileId.trim()
|
|
903
1054
|
? params.legacyParams.authProfileId.trim()
|
|
904
1055
|
: undefined;
|
|
905
|
-
// When LCM selects a dedicated summarizer model/provider, do not leak the
|
|
906
|
-
// active session's auth profile into that separate credential lookup.
|
|
907
|
-
const authProfileId = resolvedSummary === undefined ? legacyAuthProfileId : undefined;
|
|
908
1056
|
const agentDir =
|
|
909
1057
|
typeof params.legacyParams.agentDir === "string" && params.legacyParams.agentDir.trim()
|
|
910
1058
|
? params.legacyParams.agentDir.trim()
|
|
911
1059
|
: undefined;
|
|
912
|
-
const providerApi = resolveProviderApiFromLegacyConfig(params.legacyParams.config, provider);
|
|
913
1060
|
|
|
914
1061
|
const condensedTargetTokens =
|
|
915
1062
|
Number.isFinite(params.deps.config.condensedTargetTokens) &&
|
|
916
1063
|
params.deps.config.condensedTargetTokens > 0
|
|
917
1064
|
? params.deps.config.condensedTargetTokens
|
|
918
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;
|
|
919
1071
|
|
|
920
1072
|
const fn: LcmSummarizeFn = async (
|
|
921
1073
|
text: string,
|
|
@@ -928,15 +1080,11 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
928
1080
|
|
|
929
1081
|
const mode: SummaryMode = aggressive ? "aggressive" : "normal";
|
|
930
1082
|
const isCondensed = options?.isCondensed === true;
|
|
931
|
-
const apiKey = await params.deps.getApiKey(provider, model, {
|
|
932
|
-
profileId: authProfileId,
|
|
933
|
-
agentDir,
|
|
934
|
-
runtimeConfig: params.legacyParams.config,
|
|
935
|
-
});
|
|
936
1083
|
const targetTokens = resolveTargetTokens({
|
|
937
1084
|
inputTokens: estimateTokens(text),
|
|
938
1085
|
mode,
|
|
939
1086
|
isCondensed,
|
|
1087
|
+
leafTargetTokens,
|
|
940
1088
|
condensedTargetTokens,
|
|
941
1089
|
});
|
|
942
1090
|
const prompt = isCondensed
|
|
@@ -958,95 +1106,30 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
958
1106
|
customInstructions: params.customInstructions,
|
|
959
1107
|
});
|
|
960
1108
|
|
|
961
|
-
let
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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,
|
|
969
1120
|
agentDir,
|
|
970
1121
|
runtimeConfig: params.legacyParams.config,
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
temperature: aggressive ? 0.1 : 0.2,
|
|
980
|
-
}), SUMMARIZER_TIMEOUT_MS, "initial");
|
|
981
|
-
} catch (err) {
|
|
982
|
-
const authFailure = extractProviderAuthFailure(err);
|
|
983
|
-
if (authFailure) {
|
|
984
|
-
console.warn(buildProviderAuthWarning({ provider, model, failure: authFailure }));
|
|
985
|
-
return "";
|
|
986
|
-
}
|
|
987
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
988
|
-
const isTimeout = errMsg.includes("summarizer timeout");
|
|
989
|
-
console.warn(
|
|
990
|
-
`[lcm] summarizer ${isTimeout ? "timed out" : "failed"}; provider=${provider}; model=${model}; timeout=${SUMMARIZER_TIMEOUT_MS}ms; error=${errMsg}`,
|
|
991
|
-
);
|
|
992
|
-
if (err instanceof SummarizerTimeoutError) {
|
|
993
|
-
console.error(
|
|
994
|
-
`[lcm] summarizer timed out; provider=${provider}; model=${model}; source=fallback`,
|
|
995
|
-
);
|
|
996
|
-
return buildDeterministicFallbackSummary(text, targetTokens);
|
|
997
|
-
}
|
|
998
|
-
return "";
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
const authFailure = extractProviderAuthFailure(result);
|
|
1002
|
-
if (authFailure) {
|
|
1003
|
-
console.warn(buildProviderAuthWarning({ provider, model, failure: authFailure }));
|
|
1004
|
-
return "";
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
const normalized = normalizeCompletionSummary(result.content);
|
|
1008
|
-
let summary = normalized.summary;
|
|
1009
|
-
let summarySource: "content" | "envelope" | "retry" | "fallback" = "content";
|
|
1010
|
-
|
|
1011
|
-
// --- Empty-summary hardening: envelope → retry → deterministic fallback ---
|
|
1012
|
-
if (!summary) {
|
|
1013
|
-
// Envelope-aware extraction: some providers place summary text in
|
|
1014
|
-
// top-level response fields (output, message, response) rather than
|
|
1015
|
-
// inside the content array. Re-run normalization against the full
|
|
1016
|
-
// response envelope before spending an API call on a retry.
|
|
1017
|
-
const envelopeNormalized = normalizeCompletionSummary(result);
|
|
1018
|
-
if (envelopeNormalized.summary) {
|
|
1019
|
-
summary = envelopeNormalized.summary;
|
|
1020
|
-
summarySource = "envelope";
|
|
1021
|
-
console.error(
|
|
1022
|
-
`[lcm] recovered summary from response envelope; provider=${provider}; model=${model}; ` +
|
|
1023
|
-
`block_types=${formatBlockTypes(envelopeNormalized.blockTypes)}; source=envelope`,
|
|
1024
|
-
);
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
if (!summary) {
|
|
1029
|
-
const responseDiag = extractResponseDiagnostics(result);
|
|
1030
|
-
const diagParts = [
|
|
1031
|
-
`[lcm] empty normalized summary on first attempt`,
|
|
1032
|
-
`provider=${provider}`,
|
|
1033
|
-
`model=${model}`,
|
|
1034
|
-
`block_types=${formatBlockTypes(normalized.blockTypes)}`,
|
|
1035
|
-
`response_blocks=${result.content.length}`,
|
|
1036
|
-
];
|
|
1037
|
-
if (responseDiag) {
|
|
1038
|
-
diagParts.push(responseDiag);
|
|
1039
|
-
}
|
|
1040
|
-
console.error(`${diagParts.join("; ")}; retrying with conservative settings`);
|
|
1041
|
-
|
|
1042
|
-
// Single retry with conservative parameters: low temperature and low
|
|
1043
|
-
// reasoning budget to coax a textual response from providers that
|
|
1044
|
-
// sometimes return reasoning-only or empty blocks on the first pass.
|
|
1045
|
-
try {
|
|
1046
|
-
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({
|
|
1047
1130
|
provider,
|
|
1048
1131
|
model,
|
|
1049
|
-
apiKey,
|
|
1132
|
+
apiKey: requestApiKey,
|
|
1050
1133
|
providerApi,
|
|
1051
1134
|
authProfileId,
|
|
1052
1135
|
agentDir,
|
|
@@ -1059,69 +1142,250 @@ export async function createLcmSummarizeFromLegacyParams(params: {
|
|
|
1059
1142
|
},
|
|
1060
1143
|
],
|
|
1061
1144
|
maxTokens: targetTokens,
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
}
|
|
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
|
+
);
|
|
1070
1157
|
|
|
1071
|
-
const
|
|
1072
|
-
|
|
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
|
+
}
|
|
1073
1168
|
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
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`,
|
|
1079
1183
|
);
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1184
|
+
return directResult;
|
|
1185
|
+
} catch (directErr) {
|
|
1186
|
+
if (directErr instanceof LcmProviderAuthError) {
|
|
1187
|
+
throw directErr;
|
|
1188
|
+
}
|
|
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;
|
|
1091
1198
|
}
|
|
1092
|
-
|
|
1199
|
+
throw directErr;
|
|
1093
1200
|
}
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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);
|
|
1099
1221
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
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");
|
|
1103
1240
|
console.warn(
|
|
1104
|
-
`[lcm]
|
|
1241
|
+
`[lcm] summarizer ${isTimeout ? "timed out" : "failed"}; provider=${provider}; model=${model}; timeout=${SUMMARIZER_TIMEOUT_MS}ms; error=${errMsg}`,
|
|
1105
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 "";
|
|
1106
1256
|
}
|
|
1107
|
-
}
|
|
1108
1257
|
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
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
|
+
}
|
|
1116
1278
|
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
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;
|
|
1121
1382
|
}
|
|
1122
1383
|
|
|
1123
|
-
|
|
1384
|
+
if (lastAuthError) {
|
|
1385
|
+
throw lastAuthError;
|
|
1386
|
+
}
|
|
1387
|
+
return "";
|
|
1124
1388
|
};
|
|
1125
1389
|
|
|
1126
|
-
return { fn, model };
|
|
1390
|
+
return { fn, model: resolvedCandidates[0]!.model };
|
|
1127
1391
|
}
|