@jonathangu/openclawbrain 0.3.0
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/LICENSE +21 -0
- package/README.md +412 -0
- package/bin/openclawbrain.js +15 -0
- package/docs/END_STATE.md +244 -0
- package/docs/EVIDENCE.md +128 -0
- package/docs/RELEASE_CONTRACT.md +91 -0
- package/docs/agent-tools.md +106 -0
- package/docs/architecture.md +224 -0
- package/docs/configuration.md +178 -0
- package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/status.json +87 -0
- package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/summary.md +16 -0
- package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/trace.json +273 -0
- package/docs/evidence/2026-03-16/3188b50c4ed30f07dea111e35ce52aabefaced63/brain-teach-session-bound/validation-report.json +652 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/channels-status.txt +31 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/config-snapshot.json +66 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/doctor.json +14 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/gateway-probe.txt +34 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/gateway-status.txt +41 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/logs.txt +428 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/status-all.txt +60 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/status.json +223 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/summary.md +13 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/trace.json +4 -0
- package/docs/evidence/2026-03-16/4941429588810da5d6f7ef1509f229f83fa08031/validation-report.json +334 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/channels-status.txt +25 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/config-snapshot.json +91 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/doctor.json +14 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/gateway-probe.txt +36 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/gateway-status.txt +44 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/logs.txt +428 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-doctor.json +10 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-sdk-probe.json +11 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/preflight-setup-only.json +12 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/summary.md +30 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/short-static-classification/validation-report.json +72 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/status-all.txt +63 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/status.json +200 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/summary.md +13 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/trace.json +4 -0
- package/docs/evidence/2026-03-16/7f8dbcb27e741abdeefd5656c210639d0acdd440/validation-report.json +311 -0
- package/docs/evidence/README.md +16 -0
- package/docs/fts5.md +161 -0
- package/docs/tui.md +506 -0
- package/index.ts +1372 -0
- package/openclaw.plugin.json +136 -0
- package/package.json +66 -0
- package/src/assembler.ts +804 -0
- package/src/brain-cli.ts +316 -0
- package/src/brain-core/decay.ts +35 -0
- package/src/brain-core/episode.ts +82 -0
- package/src/brain-core/graph.ts +321 -0
- package/src/brain-core/health.ts +116 -0
- package/src/brain-core/mutator.ts +281 -0
- package/src/brain-core/pack.ts +117 -0
- package/src/brain-core/policy.ts +153 -0
- package/src/brain-core/replay.ts +1 -0
- package/src/brain-core/teacher.ts +105 -0
- package/src/brain-core/trace.ts +40 -0
- package/src/brain-core/traverse.ts +230 -0
- package/src/brain-core/types.ts +405 -0
- package/src/brain-core/update.ts +123 -0
- package/src/brain-harvest/human.ts +46 -0
- package/src/brain-harvest/scanner.ts +98 -0
- package/src/brain-harvest/self.ts +147 -0
- package/src/brain-runtime/assembler-extension.ts +230 -0
- package/src/brain-runtime/evidence-detectors.ts +68 -0
- package/src/brain-runtime/graph-io.ts +72 -0
- package/src/brain-runtime/harvester-extension.ts +98 -0
- package/src/brain-runtime/service.ts +659 -0
- package/src/brain-runtime/tools.ts +109 -0
- package/src/brain-runtime/worker-state.ts +106 -0
- package/src/brain-runtime/worker-supervisor.ts +169 -0
- package/src/brain-store/embedding.ts +179 -0
- package/src/brain-store/init.ts +347 -0
- package/src/brain-store/migrations.ts +188 -0
- package/src/brain-store/store.ts +816 -0
- package/src/brain-worker/child-runner.ts +321 -0
- package/src/brain-worker/jobs.ts +12 -0
- package/src/brain-worker/mutation-job.ts +5 -0
- package/src/brain-worker/promotion-job.ts +5 -0
- package/src/brain-worker/protocol.ts +79 -0
- package/src/brain-worker/teacher-job.ts +5 -0
- package/src/brain-worker/update-job.ts +5 -0
- package/src/brain-worker/worker.ts +422 -0
- package/src/compaction.ts +1332 -0
- package/src/db/config.ts +265 -0
- package/src/db/connection.ts +72 -0
- package/src/db/features.ts +42 -0
- package/src/db/migration.ts +561 -0
- package/src/engine.ts +1995 -0
- package/src/expansion-auth.ts +351 -0
- package/src/expansion-policy.ts +303 -0
- package/src/expansion.ts +383 -0
- package/src/integrity.ts +600 -0
- package/src/large-files.ts +527 -0
- package/src/openclaw-bridge.ts +22 -0
- package/src/retrieval.ts +357 -0
- package/src/store/conversation-store.ts +748 -0
- package/src/store/fts5-sanitize.ts +29 -0
- package/src/store/full-text-fallback.ts +74 -0
- package/src/store/index.ts +29 -0
- package/src/store/summary-store.ts +918 -0
- package/src/summarize.ts +847 -0
- package/src/tools/common.ts +53 -0
- package/src/tools/lcm-conversation-scope.ts +76 -0
- package/src/tools/lcm-describe-tool.ts +234 -0
- package/src/tools/lcm-expand-query-tool.ts +594 -0
- package/src/tools/lcm-expand-tool.delegation.ts +556 -0
- package/src/tools/lcm-expand-tool.ts +448 -0
- package/src/tools/lcm-expansion-recursion-guard.ts +286 -0
- package/src/tools/lcm-grep-tool.ts +200 -0
- package/src/transcript-repair.ts +301 -0
- package/src/types.ts +149 -0
package/src/summarize.ts
ADDED
|
@@ -0,0 +1,847 @@
|
|
|
1
|
+
import type { LcmDependencies } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export type LcmSummarizeOptions = {
|
|
4
|
+
previousSummary?: string;
|
|
5
|
+
isCondensed?: boolean;
|
|
6
|
+
depth?: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type LcmSummarizeFn = (
|
|
10
|
+
text: string,
|
|
11
|
+
aggressive?: boolean,
|
|
12
|
+
options?: LcmSummarizeOptions,
|
|
13
|
+
) => Promise<string>;
|
|
14
|
+
|
|
15
|
+
export type LcmSummarizerLegacyParams = {
|
|
16
|
+
provider?: unknown;
|
|
17
|
+
model?: unknown;
|
|
18
|
+
config?: unknown;
|
|
19
|
+
agentDir?: unknown;
|
|
20
|
+
authProfileId?: unknown;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type SummaryMode = "normal" | "aggressive";
|
|
24
|
+
|
|
25
|
+
const DEFAULT_CONDENSED_TARGET_TOKENS = 2000;
|
|
26
|
+
const LCM_SUMMARIZER_SYSTEM_PROMPT =
|
|
27
|
+
"You are a context-compaction summarization engine. Follow user instructions exactly and return plain text summary content only.";
|
|
28
|
+
const DIAGNOSTIC_MAX_DEPTH = 4;
|
|
29
|
+
const DIAGNOSTIC_MAX_ARRAY_ITEMS = 8;
|
|
30
|
+
const DIAGNOSTIC_MAX_OBJECT_KEYS = 16;
|
|
31
|
+
const DIAGNOSTIC_MAX_CHARS = 1200;
|
|
32
|
+
const DIAGNOSTIC_SENSITIVE_KEY_PATTERN =
|
|
33
|
+
/(api[-_]?key|authorization|token|secret|password|cookie|set-cookie|private[-_]?key|bearer)/i;
|
|
34
|
+
|
|
35
|
+
/** Normalize provider ids for stable config/profile lookup. */
|
|
36
|
+
function normalizeProviderId(provider: string): string {
|
|
37
|
+
return provider.trim().toLowerCase();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve provider API override from legacy OpenClaw config.
|
|
42
|
+
*
|
|
43
|
+
* When model ids are custom/forward-compat, this hint allows deps.complete to
|
|
44
|
+
* construct a valid pi-ai Model object even if getModel(provider, model) misses.
|
|
45
|
+
*/
|
|
46
|
+
function resolveProviderApiFromLegacyConfig(
|
|
47
|
+
config: unknown,
|
|
48
|
+
provider: string,
|
|
49
|
+
): string | undefined {
|
|
50
|
+
if (!config || typeof config !== "object") {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const providers = (config as { models?: { providers?: Record<string, unknown> } }).models
|
|
54
|
+
?.providers;
|
|
55
|
+
if (!providers || typeof providers !== "object") {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const direct = providers[provider];
|
|
60
|
+
if (direct && typeof direct === "object") {
|
|
61
|
+
const api = (direct as { api?: unknown }).api;
|
|
62
|
+
if (typeof api === "string" && api.trim()) {
|
|
63
|
+
return api.trim();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const normalizedProvider = normalizeProviderId(provider);
|
|
68
|
+
for (const [entryProvider, value] of Object.entries(providers)) {
|
|
69
|
+
if (normalizeProviderId(entryProvider) !== normalizedProvider) {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!value || typeof value !== "object") {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const api = (value as { api?: unknown }).api;
|
|
76
|
+
if (typeof api === "string" && api.trim()) {
|
|
77
|
+
return api.trim();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Approximate token estimate used for target-sizing prompts. */
|
|
84
|
+
function estimateTokens(text: string): number {
|
|
85
|
+
return Math.ceil(text.length / 4);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Narrow unknown values to plain object records. */
|
|
89
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
90
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Normalize text fragments from provider-specific block shapes.
|
|
95
|
+
*
|
|
96
|
+
* Deduplicates exact repeated fragments while preserving first-seen order so
|
|
97
|
+
* providers that mirror output in multiple fields don't duplicate summaries.
|
|
98
|
+
*/
|
|
99
|
+
function normalizeTextFragments(chunks: string[]): string {
|
|
100
|
+
const normalized: string[] = [];
|
|
101
|
+
const seen = new Set<string>();
|
|
102
|
+
|
|
103
|
+
for (const chunk of chunks) {
|
|
104
|
+
const trimmed = chunk.trim();
|
|
105
|
+
if (!trimmed || seen.has(trimmed)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
seen.add(trimmed);
|
|
109
|
+
normalized.push(trimmed);
|
|
110
|
+
}
|
|
111
|
+
return normalized.join("\n").trim();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Collect all nested `type` labels for diagnostics on normalization failures. */
|
|
115
|
+
function collectBlockTypes(value: unknown, out: Set<string>): void {
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
for (const entry of value) {
|
|
118
|
+
collectBlockTypes(entry, out);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!isRecord(value)) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (typeof value.type === "string" && value.type.trim()) {
|
|
127
|
+
out.add(value.type.trim());
|
|
128
|
+
}
|
|
129
|
+
for (const nested of Object.values(value)) {
|
|
130
|
+
collectBlockTypes(nested, out);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Collect text payloads from common provider response shapes. */
|
|
135
|
+
function collectTextLikeFields(value: unknown, out: string[]): void {
|
|
136
|
+
if (Array.isArray(value)) {
|
|
137
|
+
for (const entry of value) {
|
|
138
|
+
collectTextLikeFields(entry, out);
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (!isRecord(value)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
for (const key of ["text", "output_text", "thinking"]) {
|
|
147
|
+
appendTextValue(value[key], out);
|
|
148
|
+
}
|
|
149
|
+
for (const key of ["content", "summary", "output", "message", "response"]) {
|
|
150
|
+
if (key in value) {
|
|
151
|
+
collectTextLikeFields(value[key], out);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Append raw textual values and nested text wrappers (`value`, `text`). */
|
|
157
|
+
function appendTextValue(value: unknown, out: string[]): void {
|
|
158
|
+
if (typeof value === "string") {
|
|
159
|
+
out.push(value);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (Array.isArray(value)) {
|
|
163
|
+
for (const entry of value) {
|
|
164
|
+
appendTextValue(entry, out);
|
|
165
|
+
}
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!isRecord(value)) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (typeof value.value === "string") {
|
|
173
|
+
out.push(value.value);
|
|
174
|
+
}
|
|
175
|
+
if (typeof value.text === "string") {
|
|
176
|
+
out.push(value.text);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Normalize provider completion content into a plain-text summary payload. */
|
|
181
|
+
function normalizeCompletionSummary(content: unknown): { summary: string; blockTypes: string[] } {
|
|
182
|
+
const chunks: string[] = [];
|
|
183
|
+
const blockTypeSet = new Set<string>();
|
|
184
|
+
|
|
185
|
+
collectTextLikeFields(content, chunks);
|
|
186
|
+
collectBlockTypes(content, blockTypeSet);
|
|
187
|
+
|
|
188
|
+
const blockTypes = [...blockTypeSet].sort((a, b) => a.localeCompare(b));
|
|
189
|
+
return {
|
|
190
|
+
summary: normalizeTextFragments(chunks),
|
|
191
|
+
blockTypes,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Format normalized block types for concise diagnostics. */
|
|
196
|
+
function formatBlockTypes(blockTypes: string[]): string {
|
|
197
|
+
if (blockTypes.length === 0) {
|
|
198
|
+
return "(none)";
|
|
199
|
+
}
|
|
200
|
+
return blockTypes.join(",");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Truncate long diagnostic text values to keep logs bounded and readable. */
|
|
204
|
+
function truncateDiagnosticText(value: string, maxChars = DIAGNOSTIC_MAX_CHARS): string {
|
|
205
|
+
if (value.length <= maxChars) {
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
return `${value.slice(0, maxChars)}...[truncated:${value.length - maxChars} chars]`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Build a JSON-safe, redacted, depth-limited clone for diagnostic logging. */
|
|
212
|
+
function sanitizeForDiagnostics(value: unknown, depth = 0): unknown {
|
|
213
|
+
if (depth >= DIAGNOSTIC_MAX_DEPTH) {
|
|
214
|
+
return "[max-depth]";
|
|
215
|
+
}
|
|
216
|
+
if (typeof value === "string") {
|
|
217
|
+
return truncateDiagnosticText(value);
|
|
218
|
+
}
|
|
219
|
+
if (
|
|
220
|
+
value === null ||
|
|
221
|
+
typeof value === "number" ||
|
|
222
|
+
typeof value === "boolean" ||
|
|
223
|
+
typeof value === "bigint"
|
|
224
|
+
) {
|
|
225
|
+
return value;
|
|
226
|
+
}
|
|
227
|
+
if (value === undefined) {
|
|
228
|
+
return "[undefined]";
|
|
229
|
+
}
|
|
230
|
+
if (typeof value === "function") {
|
|
231
|
+
return "[function]";
|
|
232
|
+
}
|
|
233
|
+
if (typeof value === "symbol") {
|
|
234
|
+
return "[symbol]";
|
|
235
|
+
}
|
|
236
|
+
if (Array.isArray(value)) {
|
|
237
|
+
const head = value
|
|
238
|
+
.slice(0, DIAGNOSTIC_MAX_ARRAY_ITEMS)
|
|
239
|
+
.map((entry) => sanitizeForDiagnostics(entry, depth + 1));
|
|
240
|
+
if (value.length > DIAGNOSTIC_MAX_ARRAY_ITEMS) {
|
|
241
|
+
head.push(`[+${value.length - DIAGNOSTIC_MAX_ARRAY_ITEMS} more items]`);
|
|
242
|
+
}
|
|
243
|
+
return head;
|
|
244
|
+
}
|
|
245
|
+
if (!isRecord(value)) {
|
|
246
|
+
return String(value);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const out: Record<string, unknown> = {};
|
|
250
|
+
const entries = Object.entries(value);
|
|
251
|
+
for (const [key, entry] of entries.slice(0, DIAGNOSTIC_MAX_OBJECT_KEYS)) {
|
|
252
|
+
out[key] = DIAGNOSTIC_SENSITIVE_KEY_PATTERN.test(key)
|
|
253
|
+
? "[redacted]"
|
|
254
|
+
: sanitizeForDiagnostics(entry, depth + 1);
|
|
255
|
+
}
|
|
256
|
+
if (entries.length > DIAGNOSTIC_MAX_OBJECT_KEYS) {
|
|
257
|
+
out.__truncated_keys__ = entries.length - DIAGNOSTIC_MAX_OBJECT_KEYS;
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Encode diagnostic payloads in a compact JSON string with safety guards. */
|
|
263
|
+
function formatDiagnosticPayload(value: unknown): string {
|
|
264
|
+
try {
|
|
265
|
+
const json = JSON.stringify(sanitizeForDiagnostics(value));
|
|
266
|
+
if (!json) {
|
|
267
|
+
return "\"\"";
|
|
268
|
+
}
|
|
269
|
+
return truncateDiagnosticText(json);
|
|
270
|
+
} catch {
|
|
271
|
+
return "\"[unserializable]\"";
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Extract safe diagnostic metadata from a provider response envelope.
|
|
277
|
+
*
|
|
278
|
+
* Picks common metadata fields (request id, model echo, usage counters) without
|
|
279
|
+
* leaking secrets like API keys or auth tokens. The result object from
|
|
280
|
+
* `deps.complete` is typed narrowly but real provider responses carry extra
|
|
281
|
+
* fields that are useful for debugging empty-summary incidents.
|
|
282
|
+
*/
|
|
283
|
+
function extractResponseDiagnostics(result: unknown): string {
|
|
284
|
+
if (!isRecord(result)) {
|
|
285
|
+
return "";
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const parts: string[] = [];
|
|
289
|
+
|
|
290
|
+
// Envelope-shape diagnostics for empty-block incidents.
|
|
291
|
+
const topLevelKeys = Object.keys(result).slice(0, 24);
|
|
292
|
+
if (topLevelKeys.length > 0) {
|
|
293
|
+
parts.push(`keys=${topLevelKeys.join(",")}`);
|
|
294
|
+
}
|
|
295
|
+
if ("content" in result) {
|
|
296
|
+
const contentVal = result.content;
|
|
297
|
+
if (Array.isArray(contentVal)) {
|
|
298
|
+
parts.push(`content_kind=array`);
|
|
299
|
+
parts.push(`content_len=${contentVal.length}`);
|
|
300
|
+
} else if (contentVal === null) {
|
|
301
|
+
parts.push(`content_kind=null`);
|
|
302
|
+
} else {
|
|
303
|
+
parts.push(`content_kind=${typeof contentVal}`);
|
|
304
|
+
}
|
|
305
|
+
parts.push(`content_preview=${formatDiagnosticPayload(contentVal)}`);
|
|
306
|
+
} else {
|
|
307
|
+
parts.push("content_kind=missing");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Preview common non-content payload envelopes used by provider SDKs.
|
|
311
|
+
const envelopePayload: Record<string, unknown> = {};
|
|
312
|
+
for (const key of ["summary", "output", "message", "response"]) {
|
|
313
|
+
if (key in result) {
|
|
314
|
+
envelopePayload[key] = result[key];
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (Object.keys(envelopePayload).length > 0) {
|
|
318
|
+
parts.push(`payload_preview=${formatDiagnosticPayload(envelopePayload)}`);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Request / response id — present in most provider envelopes.
|
|
322
|
+
for (const key of ["id", "request_id", "x-request-id"]) {
|
|
323
|
+
const val = result[key];
|
|
324
|
+
if (typeof val === "string" && val.trim()) {
|
|
325
|
+
parts.push(`${key}=${val.trim()}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Model echo — useful when the provider selects a different checkpoint.
|
|
330
|
+
if (typeof result.model === "string" && result.model.trim()) {
|
|
331
|
+
parts.push(`resp_model=${result.model.trim()}`);
|
|
332
|
+
}
|
|
333
|
+
if (typeof result.provider === "string" && result.provider.trim()) {
|
|
334
|
+
parts.push(`resp_provider=${result.provider.trim()}`);
|
|
335
|
+
}
|
|
336
|
+
for (const key of [
|
|
337
|
+
"request_provider",
|
|
338
|
+
"request_model",
|
|
339
|
+
"request_api",
|
|
340
|
+
"request_reasoning",
|
|
341
|
+
"request_has_system",
|
|
342
|
+
"request_temperature",
|
|
343
|
+
"request_temperature_sent",
|
|
344
|
+
]) {
|
|
345
|
+
const val = result[key];
|
|
346
|
+
if (typeof val === "string" && val.trim()) {
|
|
347
|
+
parts.push(`${key}=${val.trim()}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Usage counters — safe numeric diagnostics.
|
|
352
|
+
if (isRecord(result.usage)) {
|
|
353
|
+
const u = result.usage;
|
|
354
|
+
const tokens: string[] = [];
|
|
355
|
+
for (const k of [
|
|
356
|
+
"prompt_tokens",
|
|
357
|
+
"completion_tokens",
|
|
358
|
+
"total_tokens",
|
|
359
|
+
"input",
|
|
360
|
+
"output",
|
|
361
|
+
"cacheRead",
|
|
362
|
+
"cacheWrite",
|
|
363
|
+
]) {
|
|
364
|
+
if (typeof u[k] === "number") {
|
|
365
|
+
tokens.push(`${k}=${u[k]}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (tokens.length > 0) {
|
|
369
|
+
parts.push(tokens.join(","));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Finish reason — helps explain empty content.
|
|
374
|
+
const finishReason =
|
|
375
|
+
typeof result.finish_reason === "string"
|
|
376
|
+
? result.finish_reason
|
|
377
|
+
: typeof result.stopReason === "string"
|
|
378
|
+
? result.stopReason
|
|
379
|
+
: typeof result.stop_reason === "string"
|
|
380
|
+
? result.stop_reason
|
|
381
|
+
: undefined;
|
|
382
|
+
if (finishReason) {
|
|
383
|
+
parts.push(`finish=${finishReason}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Provider-level error payloads (most useful when finish=error and content is empty).
|
|
387
|
+
const errorMessage = result.errorMessage;
|
|
388
|
+
if (typeof errorMessage === "string" && errorMessage.trim()) {
|
|
389
|
+
parts.push(`error_message=${truncateDiagnosticText(errorMessage.trim(), 400)}`);
|
|
390
|
+
}
|
|
391
|
+
const errorPayload = result.error;
|
|
392
|
+
if (errorPayload !== undefined) {
|
|
393
|
+
parts.push(`error_preview=${formatDiagnosticPayload(errorPayload)}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return parts.join("; ");
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Resolve a practical target token count for leaf and condensed summaries.
|
|
401
|
+
* Aggressive leaf mode intentionally aims lower so compaction converges faster.
|
|
402
|
+
*/
|
|
403
|
+
function resolveTargetTokens(params: {
|
|
404
|
+
inputTokens: number;
|
|
405
|
+
mode: SummaryMode;
|
|
406
|
+
isCondensed: boolean;
|
|
407
|
+
condensedTargetTokens: number;
|
|
408
|
+
}): number {
|
|
409
|
+
if (params.isCondensed) {
|
|
410
|
+
return Math.max(512, params.condensedTargetTokens);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const { inputTokens, mode } = params;
|
|
414
|
+
if (mode === "aggressive") {
|
|
415
|
+
return Math.max(96, Math.min(640, Math.floor(inputTokens * 0.2)));
|
|
416
|
+
}
|
|
417
|
+
return Math.max(192, Math.min(1200, Math.floor(inputTokens * 0.35)));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Build a leaf (segment) summarization prompt.
|
|
422
|
+
*
|
|
423
|
+
* Normal leaf mode preserves details; aggressive leaf mode keeps only the
|
|
424
|
+
* highest-value facts needed for follow-up turns.
|
|
425
|
+
*/
|
|
426
|
+
function buildLeafSummaryPrompt(params: {
|
|
427
|
+
text: string;
|
|
428
|
+
mode: SummaryMode;
|
|
429
|
+
targetTokens: number;
|
|
430
|
+
previousSummary?: string;
|
|
431
|
+
customInstructions?: string;
|
|
432
|
+
}): string {
|
|
433
|
+
const { text, mode, targetTokens, previousSummary, customInstructions } = params;
|
|
434
|
+
const previousContext = previousSummary?.trim() || "(none)";
|
|
435
|
+
|
|
436
|
+
const policy =
|
|
437
|
+
mode === "aggressive"
|
|
438
|
+
? [
|
|
439
|
+
"Aggressive summary policy:",
|
|
440
|
+
"- Keep only durable facts and current task state.",
|
|
441
|
+
"- Remove examples, repetition, and low-value narrative details.",
|
|
442
|
+
"- Preserve explicit TODOs, blockers, decisions, and constraints.",
|
|
443
|
+
].join("\n")
|
|
444
|
+
: [
|
|
445
|
+
"Normal summary policy:",
|
|
446
|
+
"- Preserve key decisions, rationale, constraints, and active tasks.",
|
|
447
|
+
"- Keep essential technical details needed to continue work safely.",
|
|
448
|
+
"- Remove obvious repetition and conversational filler.",
|
|
449
|
+
].join("\n");
|
|
450
|
+
|
|
451
|
+
const instructionBlock = customInstructions?.trim()
|
|
452
|
+
? `Operator instructions:\n${customInstructions.trim()}`
|
|
453
|
+
: "Operator instructions: (none)";
|
|
454
|
+
|
|
455
|
+
return [
|
|
456
|
+
"You summarize a SEGMENT of an OpenClaw conversation for future model turns.",
|
|
457
|
+
"Treat this as incremental memory compaction input, not a full-conversation summary.",
|
|
458
|
+
policy,
|
|
459
|
+
instructionBlock,
|
|
460
|
+
[
|
|
461
|
+
"Output requirements:",
|
|
462
|
+
"- Plain text only.",
|
|
463
|
+
"- No preamble, headings, or markdown formatting.",
|
|
464
|
+
"- Keep it concise while preserving required details.",
|
|
465
|
+
"- Track file operations (created, modified, deleted, renamed) with file paths and current status.",
|
|
466
|
+
'- If no file operations appear, include exactly: "Files: none".',
|
|
467
|
+
'- End with exactly: "Expand for details about: <comma-separated list of what was dropped or compressed>".',
|
|
468
|
+
`- Target length: about ${targetTokens} tokens or less.`,
|
|
469
|
+
].join("\n"),
|
|
470
|
+
`<previous_context>\n${previousContext}\n</previous_context>`,
|
|
471
|
+
`<conversation_segment>\n${text}\n</conversation_segment>`,
|
|
472
|
+
].join("\n\n");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function buildD1Prompt(params: {
|
|
476
|
+
text: string;
|
|
477
|
+
targetTokens: number;
|
|
478
|
+
previousSummary?: string;
|
|
479
|
+
customInstructions?: string;
|
|
480
|
+
}): string {
|
|
481
|
+
const { text, targetTokens, previousSummary, customInstructions } = params;
|
|
482
|
+
const instructionBlock = customInstructions?.trim()
|
|
483
|
+
? `Operator instructions:\n${customInstructions.trim()}`
|
|
484
|
+
: "Operator instructions: (none)";
|
|
485
|
+
const previousContext = previousSummary?.trim();
|
|
486
|
+
const previousContextBlock = previousContext
|
|
487
|
+
? [
|
|
488
|
+
"It already has this preceding summary as context. Do not repeat information",
|
|
489
|
+
"that appears there unchanged. Focus on what is new, changed, or resolved:",
|
|
490
|
+
"",
|
|
491
|
+
`<previous_context>\n${previousContext}\n</previous_context>`,
|
|
492
|
+
].join("\n")
|
|
493
|
+
: "Focus on what matters for continuation:";
|
|
494
|
+
|
|
495
|
+
return [
|
|
496
|
+
"You are compacting leaf-level conversation summaries into a single condensed memory node.",
|
|
497
|
+
"You are preparing context for a fresh model instance that will continue this conversation.",
|
|
498
|
+
instructionBlock,
|
|
499
|
+
previousContextBlock,
|
|
500
|
+
[
|
|
501
|
+
"Preserve:",
|
|
502
|
+
"- Decisions made and their rationale when rationale matters going forward.",
|
|
503
|
+
"- Earlier decisions that were superseded, and what replaced them.",
|
|
504
|
+
"- Completed tasks/topics with outcomes.",
|
|
505
|
+
"- In-progress items with current state and what remains.",
|
|
506
|
+
"- Blockers, open questions, and unresolved tensions.",
|
|
507
|
+
"- Specific references (names, paths, URLs, identifiers) needed for continuation.",
|
|
508
|
+
"",
|
|
509
|
+
"Drop low-value detail:",
|
|
510
|
+
"- Context that has not changed from previous_context.",
|
|
511
|
+
"- Intermediate dead ends where the conclusion is already known.",
|
|
512
|
+
"- Transient states that are already resolved.",
|
|
513
|
+
"- Tool-internal mechanics and process scaffolding.",
|
|
514
|
+
"",
|
|
515
|
+
"Use plain text. No mandatory structure.",
|
|
516
|
+
"Include a timeline with timestamps (hour or half-hour) for significant events.",
|
|
517
|
+
"Present information chronologically and mark superseded decisions.",
|
|
518
|
+
'End with exactly: "Expand for details about: <comma-separated list of what was dropped or compressed>".',
|
|
519
|
+
`Target length: about ${targetTokens} tokens.`,
|
|
520
|
+
].join("\n"),
|
|
521
|
+
`<conversation_to_condense>\n${text}\n</conversation_to_condense>`,
|
|
522
|
+
].join("\n\n");
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function buildD2Prompt(params: {
|
|
526
|
+
text: string;
|
|
527
|
+
targetTokens: number;
|
|
528
|
+
customInstructions?: string;
|
|
529
|
+
}): string {
|
|
530
|
+
const { text, targetTokens, customInstructions } = params;
|
|
531
|
+
const instructionBlock = customInstructions?.trim()
|
|
532
|
+
? `Operator instructions:\n${customInstructions.trim()}`
|
|
533
|
+
: "Operator instructions: (none)";
|
|
534
|
+
|
|
535
|
+
return [
|
|
536
|
+
"You are condensing multiple session-level summaries into a higher-level memory node.",
|
|
537
|
+
"A future model should understand trajectory, not per-session minutiae.",
|
|
538
|
+
instructionBlock,
|
|
539
|
+
[
|
|
540
|
+
"Preserve:",
|
|
541
|
+
"- Decisions still in effect and their rationale.",
|
|
542
|
+
"- Decisions that evolved: what changed and why.",
|
|
543
|
+
"- Completed work with outcomes.",
|
|
544
|
+
"- Active constraints, limitations, and known issues.",
|
|
545
|
+
"- Current state of in-progress work.",
|
|
546
|
+
"",
|
|
547
|
+
"Drop:",
|
|
548
|
+
"- Session-local operational detail and process mechanics.",
|
|
549
|
+
"- Identifiers that are no longer relevant.",
|
|
550
|
+
"- Intermediate states superseded by later outcomes.",
|
|
551
|
+
"",
|
|
552
|
+
"Use plain text. Brief headers are fine if useful.",
|
|
553
|
+
"Include a timeline with dates and approximate time of day for key milestones.",
|
|
554
|
+
'End with exactly: "Expand for details about: <comma-separated list of what was dropped or compressed>".',
|
|
555
|
+
`Target length: about ${targetTokens} tokens.`,
|
|
556
|
+
].join("\n"),
|
|
557
|
+
`<conversation_to_condense>\n${text}\n</conversation_to_condense>`,
|
|
558
|
+
].join("\n\n");
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function buildD3PlusPrompt(params: {
|
|
562
|
+
text: string;
|
|
563
|
+
targetTokens: number;
|
|
564
|
+
customInstructions?: string;
|
|
565
|
+
}): string {
|
|
566
|
+
const { text, targetTokens, customInstructions } = params;
|
|
567
|
+
const instructionBlock = customInstructions?.trim()
|
|
568
|
+
? `Operator instructions:\n${customInstructions.trim()}`
|
|
569
|
+
: "Operator instructions: (none)";
|
|
570
|
+
|
|
571
|
+
return [
|
|
572
|
+
"You are creating a high-level memory node from multiple phase-level summaries.",
|
|
573
|
+
"This may persist for the rest of the conversation. Keep only durable context.",
|
|
574
|
+
instructionBlock,
|
|
575
|
+
[
|
|
576
|
+
"Preserve:",
|
|
577
|
+
"- Key decisions and rationale.",
|
|
578
|
+
"- What was accomplished and current state.",
|
|
579
|
+
"- Active constraints and hard limitations.",
|
|
580
|
+
"- Important relationships between people, systems, or concepts.",
|
|
581
|
+
"- Durable lessons learned.",
|
|
582
|
+
"",
|
|
583
|
+
"Drop:",
|
|
584
|
+
"- Operational and process detail.",
|
|
585
|
+
"- Method details unless the method itself was the decision.",
|
|
586
|
+
"- Specific references unless essential for continuation.",
|
|
587
|
+
"",
|
|
588
|
+
"Use plain text. Be concise.",
|
|
589
|
+
"Include a brief timeline with dates (or date ranges) for major milestones.",
|
|
590
|
+
'End with exactly: "Expand for details about: <comma-separated list of what was dropped or compressed>".',
|
|
591
|
+
`Target length: about ${targetTokens} tokens.`,
|
|
592
|
+
].join("\n"),
|
|
593
|
+
`<conversation_to_condense>\n${text}\n</conversation_to_condense>`,
|
|
594
|
+
].join("\n\n");
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/** Build a condensed prompt variant based on the output node depth. */
|
|
598
|
+
function buildCondensedSummaryPrompt(params: {
|
|
599
|
+
text: string;
|
|
600
|
+
targetTokens: number;
|
|
601
|
+
depth: number;
|
|
602
|
+
previousSummary?: string;
|
|
603
|
+
customInstructions?: string;
|
|
604
|
+
}): string {
|
|
605
|
+
if (params.depth <= 1) {
|
|
606
|
+
return buildD1Prompt(params);
|
|
607
|
+
}
|
|
608
|
+
if (params.depth === 2) {
|
|
609
|
+
return buildD2Prompt(params);
|
|
610
|
+
}
|
|
611
|
+
return buildD3PlusPrompt(params);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Deterministic fallback summary when model output is empty.
|
|
616
|
+
*
|
|
617
|
+
* Keeps compaction progress monotonic instead of throwing and aborting the
|
|
618
|
+
* whole compaction pass.
|
|
619
|
+
*/
|
|
620
|
+
function buildDeterministicFallbackSummary(text: string, targetTokens: number): string {
|
|
621
|
+
const trimmed = text.trim();
|
|
622
|
+
if (!trimmed) {
|
|
623
|
+
return "";
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const maxChars = Math.max(256, targetTokens * 4);
|
|
627
|
+
if (trimmed.length <= maxChars) {
|
|
628
|
+
return trimmed;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return `${trimmed.slice(0, maxChars)}\n[LCM fallback summary; truncated for context management]`;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Builds a model-backed LCM summarize callback from runtime legacy params.
|
|
636
|
+
*
|
|
637
|
+
* Returns `undefined` when model/provider context is unavailable so callers can
|
|
638
|
+
* choose a fallback summarizer.
|
|
639
|
+
*/
|
|
640
|
+
export async function createLcmSummarizeFromLegacyParams(params: {
|
|
641
|
+
deps: LcmDependencies;
|
|
642
|
+
legacyParams: LcmSummarizerLegacyParams;
|
|
643
|
+
customInstructions?: string;
|
|
644
|
+
}): Promise<LcmSummarizeFn | undefined> {
|
|
645
|
+
const providerHint =
|
|
646
|
+
typeof params.legacyParams.provider === "string" ? params.legacyParams.provider.trim() : "";
|
|
647
|
+
const modelHint =
|
|
648
|
+
typeof params.legacyParams.model === "string" ? params.legacyParams.model.trim() : "";
|
|
649
|
+
const modelRef = modelHint || undefined;
|
|
650
|
+
|
|
651
|
+
let resolved: { provider: string; model: string };
|
|
652
|
+
try {
|
|
653
|
+
resolved = params.deps.resolveModel(modelRef, providerHint || undefined);
|
|
654
|
+
} catch (err) {
|
|
655
|
+
console.error(`[lcm] createLcmSummarize: resolveModel FAILED:`, err instanceof Error ? err.message : err);
|
|
656
|
+
return undefined;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
const { provider, model } = resolved;
|
|
660
|
+
if (!provider || !model) {
|
|
661
|
+
console.error(`[lcm] createLcmSummarize: empty provider="${provider}" or model="${model}"`);
|
|
662
|
+
return undefined;
|
|
663
|
+
}
|
|
664
|
+
const authProfileId =
|
|
665
|
+
typeof params.legacyParams.authProfileId === "string" &&
|
|
666
|
+
params.legacyParams.authProfileId.trim()
|
|
667
|
+
? params.legacyParams.authProfileId.trim()
|
|
668
|
+
: undefined;
|
|
669
|
+
const agentDir =
|
|
670
|
+
typeof params.legacyParams.agentDir === "string" && params.legacyParams.agentDir.trim()
|
|
671
|
+
? params.legacyParams.agentDir.trim()
|
|
672
|
+
: undefined;
|
|
673
|
+
const providerApi = resolveProviderApiFromLegacyConfig(params.legacyParams.config, provider);
|
|
674
|
+
|
|
675
|
+
const condensedTargetTokens =
|
|
676
|
+
Number.isFinite(params.deps.config.condensedTargetTokens) &&
|
|
677
|
+
params.deps.config.condensedTargetTokens > 0
|
|
678
|
+
? params.deps.config.condensedTargetTokens
|
|
679
|
+
: DEFAULT_CONDENSED_TARGET_TOKENS;
|
|
680
|
+
|
|
681
|
+
return async (
|
|
682
|
+
text: string,
|
|
683
|
+
aggressive?: boolean,
|
|
684
|
+
options?: LcmSummarizeOptions,
|
|
685
|
+
): Promise<string> => {
|
|
686
|
+
if (!text.trim()) {
|
|
687
|
+
return "";
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const mode: SummaryMode = aggressive ? "aggressive" : "normal";
|
|
691
|
+
const isCondensed = options?.isCondensed === true;
|
|
692
|
+
const apiKey = await params.deps.getApiKey(provider, model, {
|
|
693
|
+
profileId: authProfileId,
|
|
694
|
+
});
|
|
695
|
+
const targetTokens = resolveTargetTokens({
|
|
696
|
+
inputTokens: estimateTokens(text),
|
|
697
|
+
mode,
|
|
698
|
+
isCondensed,
|
|
699
|
+
condensedTargetTokens,
|
|
700
|
+
});
|
|
701
|
+
const prompt = isCondensed
|
|
702
|
+
? buildCondensedSummaryPrompt({
|
|
703
|
+
text,
|
|
704
|
+
targetTokens,
|
|
705
|
+
depth:
|
|
706
|
+
typeof options?.depth === "number" && Number.isFinite(options.depth)
|
|
707
|
+
? Math.max(1, Math.floor(options.depth))
|
|
708
|
+
: 1,
|
|
709
|
+
previousSummary: options?.previousSummary,
|
|
710
|
+
customInstructions: params.customInstructions,
|
|
711
|
+
})
|
|
712
|
+
: buildLeafSummaryPrompt({
|
|
713
|
+
text,
|
|
714
|
+
mode,
|
|
715
|
+
targetTokens,
|
|
716
|
+
previousSummary: options?.previousSummary,
|
|
717
|
+
customInstructions: params.customInstructions,
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
const result = await params.deps.complete({
|
|
721
|
+
provider,
|
|
722
|
+
model,
|
|
723
|
+
apiKey,
|
|
724
|
+
providerApi,
|
|
725
|
+
authProfileId,
|
|
726
|
+
agentDir,
|
|
727
|
+
runtimeConfig: params.legacyParams.config,
|
|
728
|
+
system: LCM_SUMMARIZER_SYSTEM_PROMPT,
|
|
729
|
+
messages: [
|
|
730
|
+
{
|
|
731
|
+
role: "user",
|
|
732
|
+
content: prompt,
|
|
733
|
+
},
|
|
734
|
+
],
|
|
735
|
+
maxTokens: targetTokens,
|
|
736
|
+
temperature: aggressive ? 0.1 : 0.2,
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const normalized = normalizeCompletionSummary(result.content);
|
|
740
|
+
let summary = normalized.summary;
|
|
741
|
+
let summarySource: "content" | "envelope" | "retry" | "fallback" = "content";
|
|
742
|
+
|
|
743
|
+
// --- Empty-summary hardening: envelope → retry → deterministic fallback ---
|
|
744
|
+
if (!summary) {
|
|
745
|
+
// Envelope-aware extraction: some providers place summary text in
|
|
746
|
+
// top-level response fields (output, message, response) rather than
|
|
747
|
+
// inside the content array. Re-run normalization against the full
|
|
748
|
+
// response envelope before spending an API call on a retry.
|
|
749
|
+
const envelopeNormalized = normalizeCompletionSummary(result);
|
|
750
|
+
if (envelopeNormalized.summary) {
|
|
751
|
+
summary = envelopeNormalized.summary;
|
|
752
|
+
summarySource = "envelope";
|
|
753
|
+
console.error(
|
|
754
|
+
`[lcm] recovered summary from response envelope; provider=${provider}; model=${model}; ` +
|
|
755
|
+
`block_types=${formatBlockTypes(envelopeNormalized.blockTypes)}; source=envelope`,
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (!summary) {
|
|
761
|
+
const responseDiag = extractResponseDiagnostics(result);
|
|
762
|
+
const diagParts = [
|
|
763
|
+
`[lcm] empty normalized summary on first attempt`,
|
|
764
|
+
`provider=${provider}`,
|
|
765
|
+
`model=${model}`,
|
|
766
|
+
`block_types=${formatBlockTypes(normalized.blockTypes)}`,
|
|
767
|
+
`response_blocks=${result.content.length}`,
|
|
768
|
+
];
|
|
769
|
+
if (responseDiag) {
|
|
770
|
+
diagParts.push(responseDiag);
|
|
771
|
+
}
|
|
772
|
+
console.error(`${diagParts.join("; ")}; retrying with conservative settings`);
|
|
773
|
+
|
|
774
|
+
// Single retry with conservative parameters: low temperature and low
|
|
775
|
+
// reasoning budget to coax a textual response from providers that
|
|
776
|
+
// sometimes return reasoning-only or empty blocks on the first pass.
|
|
777
|
+
try {
|
|
778
|
+
const retryResult = await params.deps.complete({
|
|
779
|
+
provider,
|
|
780
|
+
model,
|
|
781
|
+
apiKey,
|
|
782
|
+
providerApi,
|
|
783
|
+
authProfileId,
|
|
784
|
+
agentDir,
|
|
785
|
+
runtimeConfig: params.legacyParams.config,
|
|
786
|
+
system: LCM_SUMMARIZER_SYSTEM_PROMPT,
|
|
787
|
+
messages: [
|
|
788
|
+
{
|
|
789
|
+
role: "user",
|
|
790
|
+
content: prompt,
|
|
791
|
+
},
|
|
792
|
+
],
|
|
793
|
+
maxTokens: targetTokens,
|
|
794
|
+
temperature: 0.05,
|
|
795
|
+
reasoning: "low",
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
const retryNormalized = normalizeCompletionSummary(retryResult.content);
|
|
799
|
+
summary = retryNormalized.summary;
|
|
800
|
+
|
|
801
|
+
if (summary) {
|
|
802
|
+
summarySource = "retry";
|
|
803
|
+
console.error(
|
|
804
|
+
`[lcm] retry succeeded; provider=${provider}; model=${model}; ` +
|
|
805
|
+
`block_types=${formatBlockTypes(retryNormalized.blockTypes)}; source=retry`,
|
|
806
|
+
);
|
|
807
|
+
} else {
|
|
808
|
+
const retryDiag = extractResponseDiagnostics(retryResult);
|
|
809
|
+
const retryParts = [
|
|
810
|
+
`[lcm] retry also returned empty summary`,
|
|
811
|
+
`provider=${provider}`,
|
|
812
|
+
`model=${model}`,
|
|
813
|
+
`block_types=${formatBlockTypes(retryNormalized.blockTypes)}`,
|
|
814
|
+
`response_blocks=${retryResult.content.length}`,
|
|
815
|
+
];
|
|
816
|
+
if (retryDiag) {
|
|
817
|
+
retryParts.push(retryDiag);
|
|
818
|
+
}
|
|
819
|
+
console.error(`${retryParts.join("; ")}; falling back to truncation`);
|
|
820
|
+
}
|
|
821
|
+
} catch (retryErr) {
|
|
822
|
+
// Retry is best-effort; log and proceed to deterministic fallback.
|
|
823
|
+
console.error(
|
|
824
|
+
`[lcm] retry failed; provider=${provider} model=${model}; error=${
|
|
825
|
+
retryErr instanceof Error ? retryErr.message : String(retryErr)
|
|
826
|
+
}; falling back to truncation`,
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (!summary) {
|
|
832
|
+
summarySource = "fallback";
|
|
833
|
+
console.error(
|
|
834
|
+
`[lcm] all extraction attempts exhausted; provider=${provider}; model=${model}; source=fallback`,
|
|
835
|
+
);
|
|
836
|
+
return buildDeterministicFallbackSummary(text, targetTokens);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
if (summarySource !== "content") {
|
|
840
|
+
console.error(
|
|
841
|
+
`[lcm] summary resolved via non-content path; provider=${provider}; model=${model}; source=${summarySource}`,
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
return summary;
|
|
846
|
+
};
|
|
847
|
+
}
|