@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
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
stampDelegatedExpansionContext,
|
|
22
22
|
} from "./lcm-expansion-recursion-guard.js";
|
|
23
23
|
|
|
24
|
-
const
|
|
24
|
+
const DEFAULT_DELEGATED_WAIT_TIMEOUT_MS = 120_000;
|
|
25
25
|
const GATEWAY_TIMEOUT_MS = 10_000;
|
|
26
26
|
const DEFAULT_MAX_ANSWER_TOKENS = 2_000;
|
|
27
27
|
|
|
@@ -75,16 +75,98 @@ type ExpandQueryReply = {
|
|
|
75
75
|
truncated: boolean;
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
+
type ParsedExpandQueryReply =
|
|
79
|
+
| {
|
|
80
|
+
ok: true;
|
|
81
|
+
value: ExpandQueryReply;
|
|
82
|
+
}
|
|
83
|
+
| {
|
|
84
|
+
ok: false;
|
|
85
|
+
error: string;
|
|
86
|
+
};
|
|
87
|
+
|
|
78
88
|
type SummaryCandidate = {
|
|
79
89
|
summaryId: string;
|
|
80
90
|
conversationId: number;
|
|
91
|
+
requiresMessageExpansion: boolean;
|
|
81
92
|
};
|
|
82
93
|
|
|
94
|
+
function collectExpansionFailureText(value: unknown, parts: string[], depth = 0): void {
|
|
95
|
+
if (depth > 3 || value == null) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (typeof value === "string") {
|
|
99
|
+
const trimmed = value.trim();
|
|
100
|
+
if (trimmed) {
|
|
101
|
+
parts.push(trimmed);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
106
|
+
parts.push(String(value));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (value instanceof Error) {
|
|
110
|
+
if (value.message.trim()) {
|
|
111
|
+
parts.push(value.message.trim());
|
|
112
|
+
}
|
|
113
|
+
collectExpansionFailureText(value.cause, parts, depth + 1);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (Array.isArray(value)) {
|
|
117
|
+
for (const entry of value) {
|
|
118
|
+
collectExpansionFailureText(entry, parts, depth + 1);
|
|
119
|
+
}
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (typeof value === "object") {
|
|
123
|
+
const record = value as Record<string, unknown>;
|
|
124
|
+
for (const key of ["message", "error", "reason", "details", "response", "cause", "code"]) {
|
|
125
|
+
collectExpansionFailureText(record[key], parts, depth + 1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function formatExpansionFailure(error: unknown): string {
|
|
131
|
+
const parts: string[] = [];
|
|
132
|
+
collectExpansionFailureText(error, parts);
|
|
133
|
+
const message = parts.join(" ").replace(/\s+/g, " ").trim();
|
|
134
|
+
if (message) {
|
|
135
|
+
return message;
|
|
136
|
+
}
|
|
137
|
+
if (typeof error === "string" && error.trim()) {
|
|
138
|
+
return error.trim();
|
|
139
|
+
}
|
|
140
|
+
return "Delegated expansion query failed.";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function shouldRetryWithoutOverride(message: string): boolean {
|
|
144
|
+
const normalized = message.toLowerCase();
|
|
145
|
+
return [
|
|
146
|
+
"model.request",
|
|
147
|
+
"missing scopes",
|
|
148
|
+
"insufficient scope",
|
|
149
|
+
"unauthorized",
|
|
150
|
+
"not authorized",
|
|
151
|
+
"forbidden",
|
|
152
|
+
"provider/model overrides are not authorized",
|
|
153
|
+
"model override is not authorized",
|
|
154
|
+
"unknown model",
|
|
155
|
+
"model not found",
|
|
156
|
+
"invalid model",
|
|
157
|
+
"not available",
|
|
158
|
+
"not supported",
|
|
159
|
+
"401",
|
|
160
|
+
"403",
|
|
161
|
+
].some((signal) => normalized.includes(signal));
|
|
162
|
+
}
|
|
163
|
+
|
|
83
164
|
/**
|
|
84
165
|
* Build the sub-agent task message for delegated expansion and prompt answering.
|
|
85
166
|
*/
|
|
86
167
|
function buildDelegatedExpandQueryTask(params: {
|
|
87
168
|
summaryIds: string[];
|
|
169
|
+
messageBackedSummaryIds: string[];
|
|
88
170
|
conversationId: number;
|
|
89
171
|
query?: string;
|
|
90
172
|
prompt: string;
|
|
@@ -95,6 +177,10 @@ function buildDelegatedExpandQueryTask(params: {
|
|
|
95
177
|
originSessionKey: string;
|
|
96
178
|
}) {
|
|
97
179
|
const seedSummaryIds = params.summaryIds.length > 0 ? params.summaryIds.join(", ") : "(none)";
|
|
180
|
+
const messageBackedSummaryIds =
|
|
181
|
+
params.messageBackedSummaryIds.length > 0
|
|
182
|
+
? params.messageBackedSummaryIds.join(", ")
|
|
183
|
+
: "(none)";
|
|
98
184
|
return [
|
|
99
185
|
"You are an autonomous LCM retrieval navigator. Plan and execute retrieval before answering.",
|
|
100
186
|
"",
|
|
@@ -102,6 +188,7 @@ function buildDelegatedExpandQueryTask(params: {
|
|
|
102
188
|
`Conversation scope: ${params.conversationId}`,
|
|
103
189
|
`Expansion token budget (total across this run): ${params.tokenCap}`,
|
|
104
190
|
`Seed summary IDs: ${seedSummaryIds}`,
|
|
191
|
+
`Seed summaries requiring raw message expansion: ${messageBackedSummaryIds}`,
|
|
105
192
|
params.query ? `Routing query: ${params.query}` : undefined,
|
|
106
193
|
"",
|
|
107
194
|
"Strategy:",
|
|
@@ -109,7 +196,7 @@ function buildDelegatedExpandQueryTask(params: {
|
|
|
109
196
|
"2. If additional candidates are needed, use `lcm_grep` scoped to summaries.",
|
|
110
197
|
"3. Select branches that fit remaining budget; prefer high-signal paths first.",
|
|
111
198
|
"4. Call `lcm_expand` selectively (do not expand everything blindly).",
|
|
112
|
-
"5. Keep includeMessages=false by default; use includeMessages=true
|
|
199
|
+
"5. Keep includeMessages=false by default; use includeMessages=true for the message-backed seed summaries above and any other specific leaf evidence.",
|
|
113
200
|
`6. Stay within ${params.tokenCap} total expansion tokens across all lcm_expand calls.`,
|
|
114
201
|
"",
|
|
115
202
|
"User prompt to answer:",
|
|
@@ -141,24 +228,25 @@ function buildDelegatedExpandQueryTask(params: {
|
|
|
141
228
|
].join("\n");
|
|
142
229
|
}
|
|
143
230
|
|
|
231
|
+
function formatInvalidDelegatedReply(reply: string, reason: string): string {
|
|
232
|
+
const compact = reply.replace(/\s+/g, " ").trim();
|
|
233
|
+
const snippet = compact.length <= 240 ? compact : `${compact.slice(0, 240)}...`;
|
|
234
|
+
return `Delegated expansion query returned ${reason}: ${snippet}`;
|
|
235
|
+
}
|
|
236
|
+
|
|
144
237
|
/**
|
|
145
|
-
* Parse the child reply; accepts plain JSON or fenced JSON.
|
|
238
|
+
* Parse the child reply; accepts plain JSON or fenced JSON and rejects malformed fallbacks.
|
|
146
239
|
*/
|
|
147
240
|
function parseDelegatedExpandQueryReply(
|
|
148
241
|
rawReply: string | undefined,
|
|
149
242
|
fallbackExpandedSummaryCount: number,
|
|
150
|
-
):
|
|
151
|
-
const fallback: ExpandQueryReply = {
|
|
152
|
-
answer: (rawReply ?? "").trim(),
|
|
153
|
-
citedIds: [],
|
|
154
|
-
expandedSummaryCount: fallbackExpandedSummaryCount,
|
|
155
|
-
totalSourceTokens: 0,
|
|
156
|
-
truncated: false,
|
|
157
|
-
};
|
|
158
|
-
|
|
243
|
+
): ParsedExpandQueryReply {
|
|
159
244
|
const reply = rawReply?.trim();
|
|
160
245
|
if (!reply) {
|
|
161
|
-
return
|
|
246
|
+
return {
|
|
247
|
+
ok: false,
|
|
248
|
+
error: "Delegated expansion query returned an empty reply.",
|
|
249
|
+
};
|
|
162
250
|
}
|
|
163
251
|
|
|
164
252
|
const candidates: string[] = [reply];
|
|
@@ -177,6 +265,12 @@ function parseDelegatedExpandQueryReply(
|
|
|
177
265
|
truncated?: unknown;
|
|
178
266
|
};
|
|
179
267
|
const answer = typeof parsed.answer === "string" ? parsed.answer.trim() : "";
|
|
268
|
+
if (!answer) {
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
error: formatInvalidDelegatedReply(reply, 'JSON without a non-empty "answer"'),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
180
274
|
const citedIds = normalizeSummaryIds(
|
|
181
275
|
Array.isArray(parsed.citedIds)
|
|
182
276
|
? parsed.citedIds.filter((value): value is string => typeof value === "string")
|
|
@@ -194,18 +288,24 @@ function parseDelegatedExpandQueryReply(
|
|
|
194
288
|
const truncated = parsed.truncated === true;
|
|
195
289
|
|
|
196
290
|
return {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
291
|
+
ok: true,
|
|
292
|
+
value: {
|
|
293
|
+
answer,
|
|
294
|
+
citedIds,
|
|
295
|
+
expandedSummaryCount,
|
|
296
|
+
totalSourceTokens,
|
|
297
|
+
truncated,
|
|
298
|
+
},
|
|
202
299
|
};
|
|
203
300
|
} catch {
|
|
204
301
|
// Try next candidate.
|
|
205
302
|
}
|
|
206
303
|
}
|
|
207
304
|
|
|
208
|
-
return
|
|
305
|
+
return {
|
|
306
|
+
ok: false,
|
|
307
|
+
error: formatInvalidDelegatedReply(reply, "non-JSON output"),
|
|
308
|
+
};
|
|
209
309
|
}
|
|
210
310
|
|
|
211
311
|
/**
|
|
@@ -246,6 +346,22 @@ function resolveSourceConversationId(params: {
|
|
|
246
346
|
);
|
|
247
347
|
}
|
|
248
348
|
|
|
349
|
+
function upsertSummaryCandidate(
|
|
350
|
+
candidates: Map<string, SummaryCandidate>,
|
|
351
|
+
candidate: SummaryCandidate,
|
|
352
|
+
): void {
|
|
353
|
+
const existing = candidates.get(candidate.summaryId);
|
|
354
|
+
if (!existing) {
|
|
355
|
+
candidates.set(candidate.summaryId, candidate);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
candidates.set(candidate.summaryId, {
|
|
359
|
+
...existing,
|
|
360
|
+
requiresMessageExpansion:
|
|
361
|
+
existing.requiresMessageExpansion || candidate.requiresMessageExpansion,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
249
365
|
/**
|
|
250
366
|
* Resolve summary candidates from explicit IDs and/or query matches.
|
|
251
367
|
*/
|
|
@@ -263,13 +379,15 @@ async function resolveSummaryCandidates(params: {
|
|
|
263
379
|
if (!described || described.type !== "summary" || !described.summary) {
|
|
264
380
|
throw new Error(`Summary not found: ${summaryId}`);
|
|
265
381
|
}
|
|
266
|
-
candidates
|
|
382
|
+
upsertSummaryCandidate(candidates, {
|
|
267
383
|
summaryId,
|
|
268
384
|
conversationId: described.summary.conversationId,
|
|
385
|
+
requiresMessageExpansion: false,
|
|
269
386
|
});
|
|
270
387
|
}
|
|
271
388
|
|
|
272
389
|
if (params.query) {
|
|
390
|
+
const summaryStore = params.lcm.getSummaryStore();
|
|
273
391
|
const grepResult = await retrieval.grep({
|
|
274
392
|
query: params.query,
|
|
275
393
|
mode: "full_text",
|
|
@@ -277,11 +395,46 @@ async function resolveSummaryCandidates(params: {
|
|
|
277
395
|
conversationId: params.conversationId,
|
|
278
396
|
});
|
|
279
397
|
for (const summary of grepResult.summaries) {
|
|
280
|
-
candidates
|
|
398
|
+
upsertSummaryCandidate(candidates, {
|
|
281
399
|
summaryId: summary.summaryId,
|
|
282
400
|
conversationId: summary.conversationId,
|
|
401
|
+
requiresMessageExpansion: false,
|
|
283
402
|
});
|
|
284
403
|
}
|
|
404
|
+
|
|
405
|
+
if (grepResult.summaries.length === 0 && typeof params.conversationId === "number") {
|
|
406
|
+
const maxDepth = await summaryStore.getConversationMaxSummaryDepth(params.conversationId);
|
|
407
|
+
if (typeof maxDepth === "number" && maxDepth <= 1) {
|
|
408
|
+
const messageResult = await retrieval.grep({
|
|
409
|
+
query: params.query,
|
|
410
|
+
mode: "full_text",
|
|
411
|
+
scope: "messages",
|
|
412
|
+
conversationId: params.conversationId,
|
|
413
|
+
});
|
|
414
|
+
const messageIds = messageResult.messages.map((message) => message.messageId);
|
|
415
|
+
const leafLinks = await summaryStore.getLeafSummaryLinksForMessageIds(
|
|
416
|
+
params.conversationId,
|
|
417
|
+
messageIds,
|
|
418
|
+
);
|
|
419
|
+
const summaryIdsByMessageId = new Map<number, string[]>();
|
|
420
|
+
for (const link of leafLinks) {
|
|
421
|
+
const linkedSummaryIds = summaryIdsByMessageId.get(link.messageId) ?? [];
|
|
422
|
+
if (!linkedSummaryIds.includes(link.summaryId)) {
|
|
423
|
+
linkedSummaryIds.push(link.summaryId);
|
|
424
|
+
summaryIdsByMessageId.set(link.messageId, linkedSummaryIds);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
for (const message of messageResult.messages) {
|
|
428
|
+
for (const summaryId of summaryIdsByMessageId.get(message.messageId) ?? []) {
|
|
429
|
+
upsertSummaryCandidate(candidates, {
|
|
430
|
+
summaryId,
|
|
431
|
+
conversationId: params.conversationId,
|
|
432
|
+
requiresMessageExpansion: true,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
285
438
|
}
|
|
286
439
|
|
|
287
440
|
return Array.from(candidates.values());
|
|
@@ -297,6 +450,10 @@ export function createLcmExpandQueryTool(input: {
|
|
|
297
450
|
/** Session key for scope fallback when sessionId is unavailable. */
|
|
298
451
|
sessionKey?: string;
|
|
299
452
|
}): AnyAgentTool {
|
|
453
|
+
const delegatedWaitTimeoutMs =
|
|
454
|
+
input.deps.config.delegationTimeoutMs || DEFAULT_DELEGATED_WAIT_TIMEOUT_MS;
|
|
455
|
+
const delegatedWaitTimeoutSeconds = Math.ceil(delegatedWaitTimeoutMs / 1000);
|
|
456
|
+
|
|
300
457
|
return {
|
|
301
458
|
name: "lcm_expand_query",
|
|
302
459
|
label: "LCM Expand Query",
|
|
@@ -401,9 +558,6 @@ export function createLcmExpandQueryTool(input: {
|
|
|
401
558
|
});
|
|
402
559
|
}
|
|
403
560
|
|
|
404
|
-
let childSessionKey = "";
|
|
405
|
-
let grantCreated = false;
|
|
406
|
-
|
|
407
561
|
try {
|
|
408
562
|
const candidates = await resolveSummaryCandidates({
|
|
409
563
|
lcm: input.lcm,
|
|
@@ -438,6 +592,15 @@ export function createLcmExpandQueryTool(input: {
|
|
|
438
592
|
.filter((candidate) => candidate.conversationId === sourceConversationId)
|
|
439
593
|
.map((candidate) => candidate.summaryId),
|
|
440
594
|
);
|
|
595
|
+
const messageBackedSummaryIds = normalizeSummaryIds(
|
|
596
|
+
candidates
|
|
597
|
+
.filter(
|
|
598
|
+
(candidate) =>
|
|
599
|
+
candidate.conversationId === sourceConversationId &&
|
|
600
|
+
candidate.requiresMessageExpansion,
|
|
601
|
+
)
|
|
602
|
+
.map((candidate) => candidate.summaryId),
|
|
603
|
+
);
|
|
441
604
|
|
|
442
605
|
if (summaryIds.length === 0) {
|
|
443
606
|
return jsonResult({
|
|
@@ -448,28 +611,12 @@ export function createLcmExpandQueryTool(input: {
|
|
|
448
611
|
const requesterAgentId = input.deps.normalizeAgentId(
|
|
449
612
|
input.deps.parseAgentSessionKey(callerSessionKey)?.agentId,
|
|
450
613
|
);
|
|
451
|
-
childSessionKey = `agent:${requesterAgentId}:subagent:${crypto.randomUUID()}`;
|
|
452
614
|
const childExpansionDepth = resolveNextExpansionDepth(callerSessionKey);
|
|
453
615
|
const originSessionKey = recursionCheck.originSessionKey || callerSessionKey || "main";
|
|
454
616
|
|
|
455
|
-
createDelegatedExpansionGrant({
|
|
456
|
-
delegatedSessionKey: childSessionKey,
|
|
457
|
-
issuerSessionId: callerSessionKey || "main",
|
|
458
|
-
allowedConversationIds: [sourceConversationId],
|
|
459
|
-
tokenCap: expansionTokenCap,
|
|
460
|
-
ttlMs: DELEGATED_WAIT_TIMEOUT_MS + 30_000,
|
|
461
|
-
});
|
|
462
|
-
stampDelegatedExpansionContext({
|
|
463
|
-
sessionKey: childSessionKey,
|
|
464
|
-
requestId,
|
|
465
|
-
expansionDepth: childExpansionDepth,
|
|
466
|
-
originSessionKey,
|
|
467
|
-
stampedBy: "lcm_expand_query",
|
|
468
|
-
});
|
|
469
|
-
grantCreated = true;
|
|
470
|
-
|
|
471
617
|
const task = buildDelegatedExpandQueryTask({
|
|
472
618
|
summaryIds,
|
|
619
|
+
messageBackedSummaryIds,
|
|
473
620
|
conversationId: sourceConversationId,
|
|
474
621
|
query: query || undefined,
|
|
475
622
|
prompt,
|
|
@@ -480,118 +627,166 @@ export function createLcmExpandQueryTool(input: {
|
|
|
480
627
|
originSessionKey,
|
|
481
628
|
});
|
|
482
629
|
|
|
483
|
-
const childIdem = crypto.randomUUID();
|
|
484
630
|
const expansionProvider = input.deps.config.expansionProvider || undefined;
|
|
485
631
|
const expansionModel = input.deps.config.expansionModel || undefined;
|
|
486
|
-
const
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
maxDepth: 8,
|
|
499
|
-
taskSummary: "Run lcm_expand and return prompt-focused JSON answer",
|
|
500
|
-
}),
|
|
501
|
-
},
|
|
502
|
-
timeoutMs: GATEWAY_TIMEOUT_MS,
|
|
503
|
-
})) as { runId?: string };
|
|
504
|
-
|
|
505
|
-
const runId = typeof response?.runId === "string" ? response.runId.trim() : "";
|
|
506
|
-
if (!runId) {
|
|
507
|
-
return jsonResult({
|
|
508
|
-
error: "Delegated expansion did not return a runId.",
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const wait = (await input.deps.callGateway({
|
|
513
|
-
method: "agent.wait",
|
|
514
|
-
params: {
|
|
515
|
-
runId,
|
|
516
|
-
timeoutMs: DELEGATED_WAIT_TIMEOUT_MS,
|
|
517
|
-
},
|
|
518
|
-
timeoutMs: DELEGATED_WAIT_TIMEOUT_MS,
|
|
519
|
-
})) as { status?: string; error?: string };
|
|
520
|
-
const status = typeof wait?.status === "string" ? wait.status : "error";
|
|
521
|
-
if (status === "timeout") {
|
|
522
|
-
recordExpansionDelegationTelemetry({
|
|
523
|
-
deps: input.deps,
|
|
524
|
-
component: "lcm_expand_query",
|
|
525
|
-
event: "timeout",
|
|
526
|
-
requestId,
|
|
527
|
-
sessionKey: callerSessionKey,
|
|
528
|
-
expansionDepth: childExpansionDepth,
|
|
529
|
-
originSessionKey,
|
|
530
|
-
runId,
|
|
531
|
-
});
|
|
532
|
-
return jsonResult({
|
|
533
|
-
error: "lcm_expand_query timed out waiting for delegated expansion (120s).",
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
if (status !== "ok") {
|
|
537
|
-
return jsonResult({
|
|
538
|
-
error:
|
|
539
|
-
typeof wait?.error === "string" && wait.error.trim()
|
|
540
|
-
? wait.error
|
|
541
|
-
: "Delegated expansion query failed.",
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
const replyPayload = (await input.deps.callGateway({
|
|
546
|
-
method: "sessions.get",
|
|
547
|
-
params: { key: childSessionKey, limit: 80 },
|
|
548
|
-
timeoutMs: GATEWAY_TIMEOUT_MS,
|
|
549
|
-
})) as { messages?: unknown[] };
|
|
550
|
-
const reply = input.deps.readLatestAssistantReply(
|
|
551
|
-
Array.isArray(replyPayload.messages) ? replyPayload.messages : [],
|
|
552
|
-
);
|
|
553
|
-
const parsed = parseDelegatedExpandQueryReply(reply, summaryIds.length);
|
|
554
|
-
recordExpansionDelegationTelemetry({
|
|
555
|
-
deps: input.deps,
|
|
556
|
-
component: "lcm_expand_query",
|
|
557
|
-
event: "success",
|
|
558
|
-
requestId,
|
|
559
|
-
sessionKey: callerSessionKey,
|
|
560
|
-
expansionDepth: childExpansionDepth,
|
|
561
|
-
originSessionKey,
|
|
562
|
-
runId,
|
|
563
|
-
});
|
|
632
|
+
const canonicalExpansionModel = expansionModel?.includes("/") ? expansionModel : undefined;
|
|
633
|
+
const delegatedOverrideProvider = canonicalExpansionModel ? undefined : expansionProvider;
|
|
634
|
+
const delegatedOverrideModel = canonicalExpansionModel || expansionModel;
|
|
635
|
+
const configuredOverrideLabel =
|
|
636
|
+
delegatedOverrideProvider && delegatedOverrideModel
|
|
637
|
+
? `${delegatedOverrideProvider}/${delegatedOverrideModel}`
|
|
638
|
+
: delegatedOverrideModel || delegatedOverrideProvider || "configured override";
|
|
639
|
+
|
|
640
|
+
const runDelegatedQuery = async (provider?: string, model?: string) => {
|
|
641
|
+
const childSessionKey = `agent:${requesterAgentId}:subagent:${crypto.randomUUID()}`;
|
|
642
|
+
const childIdem = crypto.randomUUID();
|
|
643
|
+
let grantCreated = false;
|
|
564
644
|
|
|
565
|
-
return jsonResult({
|
|
566
|
-
answer: parsed.answer,
|
|
567
|
-
citedIds: parsed.citedIds,
|
|
568
|
-
sourceConversationId,
|
|
569
|
-
expandedSummaryCount: parsed.expandedSummaryCount,
|
|
570
|
-
totalSourceTokens: parsed.totalSourceTokens,
|
|
571
|
-
truncated: parsed.truncated,
|
|
572
|
-
});
|
|
573
|
-
} catch (error) {
|
|
574
|
-
return jsonResult({
|
|
575
|
-
error: error instanceof Error ? error.message : String(error),
|
|
576
|
-
});
|
|
577
|
-
} finally {
|
|
578
|
-
if (childSessionKey) {
|
|
579
645
|
try {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
646
|
+
createDelegatedExpansionGrant({
|
|
647
|
+
delegatedSessionKey: childSessionKey,
|
|
648
|
+
issuerSessionId: callerSessionKey || "main",
|
|
649
|
+
allowedConversationIds: [sourceConversationId],
|
|
650
|
+
tokenCap: expansionTokenCap,
|
|
651
|
+
ttlMs: delegatedWaitTimeoutMs + 30_000,
|
|
652
|
+
});
|
|
653
|
+
stampDelegatedExpansionContext({
|
|
654
|
+
sessionKey: childSessionKey,
|
|
655
|
+
requestId,
|
|
656
|
+
expansionDepth: childExpansionDepth,
|
|
657
|
+
originSessionKey,
|
|
658
|
+
stampedBy: "lcm_expand_query",
|
|
659
|
+
});
|
|
660
|
+
grantCreated = true;
|
|
661
|
+
|
|
662
|
+
const response = (await input.deps.callGateway({
|
|
663
|
+
method: "agent",
|
|
664
|
+
params: {
|
|
665
|
+
message: task,
|
|
666
|
+
sessionKey: childSessionKey,
|
|
667
|
+
deliver: false,
|
|
668
|
+
lane: input.deps.agentLaneSubagent,
|
|
669
|
+
idempotencyKey: childIdem,
|
|
670
|
+
...(provider ? { provider } : {}),
|
|
671
|
+
...(model ? { model } : {}),
|
|
672
|
+
extraSystemPrompt: input.deps.buildSubagentSystemPrompt({
|
|
673
|
+
depth: 1,
|
|
674
|
+
maxDepth: 8,
|
|
675
|
+
taskSummary: "Run lcm_expand and return prompt-focused JSON answer",
|
|
676
|
+
}),
|
|
677
|
+
},
|
|
583
678
|
timeoutMs: GATEWAY_TIMEOUT_MS,
|
|
679
|
+
})) as { runId?: unknown; error?: unknown };
|
|
680
|
+
|
|
681
|
+
const runId = typeof response?.runId === "string" ? response.runId.trim() : "";
|
|
682
|
+
if (!runId) {
|
|
683
|
+
throw new Error(
|
|
684
|
+
formatExpansionFailure(response?.error ?? response)
|
|
685
|
+
|| "Delegated expansion did not return a runId.",
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const wait = (await input.deps.callGateway({
|
|
690
|
+
method: "agent.wait",
|
|
691
|
+
params: {
|
|
692
|
+
runId,
|
|
693
|
+
timeoutMs: delegatedWaitTimeoutMs,
|
|
694
|
+
},
|
|
695
|
+
timeoutMs: delegatedWaitTimeoutMs,
|
|
696
|
+
})) as { status?: string; error?: unknown };
|
|
697
|
+
const status = typeof wait?.status === "string" ? wait.status : "error";
|
|
698
|
+
if (status === "timeout") {
|
|
699
|
+
recordExpansionDelegationTelemetry({
|
|
700
|
+
deps: input.deps,
|
|
701
|
+
component: "lcm_expand_query",
|
|
702
|
+
event: "timeout",
|
|
703
|
+
requestId,
|
|
704
|
+
sessionKey: callerSessionKey,
|
|
705
|
+
expansionDepth: childExpansionDepth,
|
|
706
|
+
originSessionKey,
|
|
707
|
+
runId,
|
|
708
|
+
});
|
|
709
|
+
throw new Error(
|
|
710
|
+
`lcm_expand_query timed out waiting for delegated expansion (${delegatedWaitTimeoutSeconds}s).`,
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
if (status !== "ok") {
|
|
714
|
+
throw new Error(formatExpansionFailure(wait?.error));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const replyPayload = (await input.deps.callGateway({
|
|
718
|
+
method: "sessions.get",
|
|
719
|
+
params: { key: childSessionKey, limit: 80 },
|
|
720
|
+
timeoutMs: GATEWAY_TIMEOUT_MS,
|
|
721
|
+
})) as { messages?: unknown[] };
|
|
722
|
+
const reply = input.deps.readLatestAssistantReply(
|
|
723
|
+
Array.isArray(replyPayload.messages) ? replyPayload.messages : [],
|
|
724
|
+
);
|
|
725
|
+
const parsed = parseDelegatedExpandQueryReply(reply, summaryIds.length);
|
|
726
|
+
if (!parsed.ok) {
|
|
727
|
+
throw new Error(parsed.error);
|
|
728
|
+
}
|
|
729
|
+
recordExpansionDelegationTelemetry({
|
|
730
|
+
deps: input.deps,
|
|
731
|
+
component: "lcm_expand_query",
|
|
732
|
+
event: "success",
|
|
733
|
+
requestId,
|
|
734
|
+
sessionKey: callerSessionKey,
|
|
735
|
+
expansionDepth: childExpansionDepth,
|
|
736
|
+
originSessionKey,
|
|
737
|
+
runId,
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
return jsonResult({
|
|
741
|
+
answer: parsed.value.answer,
|
|
742
|
+
citedIds: parsed.value.citedIds,
|
|
743
|
+
sourceConversationId,
|
|
744
|
+
expandedSummaryCount: parsed.value.expandedSummaryCount,
|
|
745
|
+
totalSourceTokens: parsed.value.totalSourceTokens,
|
|
746
|
+
truncated: parsed.value.truncated,
|
|
584
747
|
});
|
|
585
|
-
}
|
|
586
|
-
|
|
748
|
+
} finally {
|
|
749
|
+
try {
|
|
750
|
+
await input.deps.callGateway({
|
|
751
|
+
method: "sessions.delete",
|
|
752
|
+
params: { key: childSessionKey, deleteTranscript: true },
|
|
753
|
+
timeoutMs: GATEWAY_TIMEOUT_MS,
|
|
754
|
+
});
|
|
755
|
+
} catch {
|
|
756
|
+
// Cleanup is best-effort.
|
|
757
|
+
}
|
|
758
|
+
if (grantCreated) {
|
|
759
|
+
revokeDelegatedExpansionGrantForSession(childSessionKey, { removeBinding: true });
|
|
760
|
+
}
|
|
761
|
+
clearDelegatedExpansionContext(childSessionKey);
|
|
587
762
|
}
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
if (!expansionProvider && !expansionModel) {
|
|
766
|
+
return await runDelegatedQuery();
|
|
588
767
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
768
|
+
|
|
769
|
+
try {
|
|
770
|
+
return await runDelegatedQuery(delegatedOverrideProvider, delegatedOverrideModel);
|
|
771
|
+
} catch (error) {
|
|
772
|
+
const failure = formatExpansionFailure(error);
|
|
773
|
+
input.deps.log.warn(
|
|
774
|
+
`[lcm] delegated expansion override failed (${configuredOverrideLabel}): ${failure}`,
|
|
775
|
+
);
|
|
776
|
+
if (!shouldRetryWithoutOverride(failure)) {
|
|
777
|
+
throw new Error(failure);
|
|
778
|
+
}
|
|
779
|
+
input.deps.log.warn(
|
|
780
|
+
`[lcm] retrying delegated expansion without provider/model override after: ${failure}`,
|
|
781
|
+
);
|
|
782
|
+
return await runDelegatedQuery();
|
|
594
783
|
}
|
|
784
|
+
} catch (error) {
|
|
785
|
+
const failure = formatExpansionFailure(error);
|
|
786
|
+
input.deps.log.error(`[lcm] delegated expansion query failed: ${failure}`);
|
|
787
|
+
return jsonResult({
|
|
788
|
+
error: failure,
|
|
789
|
+
});
|
|
595
790
|
}
|
|
596
791
|
},
|
|
597
792
|
};
|