@martian-engineering/lossless-claw 0.1.4 → 0.1.6
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/docs/configuration.md +7 -0
- package/docs/tui.md +5 -0
- package/index.ts +93 -9
- package/package.json +1 -1
- package/src/compaction.ts +36 -1
- package/src/db/migration.ts +58 -6
- package/src/expansion-auth.ts +53 -1
- package/src/retrieval.ts +44 -1
- package/src/store/summary-store.ts +122 -3
- package/src/summarize.ts +303 -4
- package/src/tools/lcm-describe-tool.ts +104 -17
- package/src/tools/lcm-expand-query-tool.ts +128 -16
- package/src/tools/lcm-expand-tool.delegation.ts +96 -0
- package/src/tools/lcm-expansion-recursion-guard.ts +286 -0
- package/src/types.ts +12 -1
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import type { LcmContextEngine } from "../engine.js";
|
|
3
|
+
import {
|
|
4
|
+
getRuntimeExpansionAuthManager,
|
|
5
|
+
resolveDelegatedExpansionGrantId,
|
|
6
|
+
} from "../expansion-auth.js";
|
|
3
7
|
import type { LcmDependencies } from "../types.js";
|
|
4
8
|
import type { AnyAgentTool } from "./common.js";
|
|
5
9
|
import { jsonResult } from "./common.js";
|
|
@@ -21,8 +25,25 @@ const LcmDescribeSchema = Type.Object({
|
|
|
21
25
|
"Set true to explicitly allow lookups across all conversations. Ignored when conversationId is provided.",
|
|
22
26
|
}),
|
|
23
27
|
),
|
|
28
|
+
tokenCap: Type.Optional(
|
|
29
|
+
Type.Number({
|
|
30
|
+
description: "Optional budget cap used for subtree manifest budget-fit annotations.",
|
|
31
|
+
minimum: 1,
|
|
32
|
+
}),
|
|
33
|
+
),
|
|
24
34
|
});
|
|
25
35
|
|
|
36
|
+
function normalizeRequestedTokenCap(value: unknown): number | undefined {
|
|
37
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
return Math.max(1, Math.trunc(value));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatIso(value: Date | null | undefined): string {
|
|
44
|
+
return value instanceof Date ? value.toISOString() : "-";
|
|
45
|
+
}
|
|
46
|
+
|
|
26
47
|
export function createLcmDescribeTool(input: {
|
|
27
48
|
deps: LcmDependencies;
|
|
28
49
|
lcm: LcmContextEngine;
|
|
@@ -77,33 +98,99 @@ export function createLcmDescribeTool(input: {
|
|
|
77
98
|
|
|
78
99
|
if (result.type === "summary" && result.summary) {
|
|
79
100
|
const s = result.summary;
|
|
101
|
+
const requestedTokenCap = normalizeRequestedTokenCap((params as Record<string, unknown>).tokenCap);
|
|
102
|
+
const sessionKey =
|
|
103
|
+
(typeof input.sessionKey === "string" ? input.sessionKey : input.sessionId)?.trim() ?? "";
|
|
104
|
+
const delegatedGrantId = input.deps.isSubagentSessionKey(sessionKey)
|
|
105
|
+
? (resolveDelegatedExpansionGrantId(sessionKey) ?? "")
|
|
106
|
+
: "";
|
|
107
|
+
const delegatedRemainingBudget =
|
|
108
|
+
delegatedGrantId !== ""
|
|
109
|
+
? getRuntimeExpansionAuthManager().getRemainingTokenBudget(delegatedGrantId)
|
|
110
|
+
: null;
|
|
111
|
+
const defaultTokenCap = Math.max(1, Math.trunc(input.deps.config.maxExpandTokens));
|
|
112
|
+
const resolvedTokenCap = (() => {
|
|
113
|
+
const base =
|
|
114
|
+
requestedTokenCap ??
|
|
115
|
+
(typeof delegatedRemainingBudget === "number" ? delegatedRemainingBudget : defaultTokenCap);
|
|
116
|
+
if (typeof delegatedRemainingBudget === "number") {
|
|
117
|
+
return Math.max(0, Math.min(base, delegatedRemainingBudget));
|
|
118
|
+
}
|
|
119
|
+
return Math.max(1, base);
|
|
120
|
+
})();
|
|
121
|
+
|
|
122
|
+
const manifestNodes = s.subtree.map((node) => {
|
|
123
|
+
const summariesOnlyCost = Math.max(0, node.tokenCount + node.descendantTokenCount);
|
|
124
|
+
const withMessagesCost = Math.max(0, summariesOnlyCost + node.sourceMessageTokenCount);
|
|
125
|
+
return {
|
|
126
|
+
summaryId: node.summaryId,
|
|
127
|
+
parentSummaryId: node.parentSummaryId,
|
|
128
|
+
depthFromRoot: node.depthFromRoot,
|
|
129
|
+
depth: node.depth,
|
|
130
|
+
kind: node.kind,
|
|
131
|
+
tokenCount: node.tokenCount,
|
|
132
|
+
descendantCount: node.descendantCount,
|
|
133
|
+
descendantTokenCount: node.descendantTokenCount,
|
|
134
|
+
sourceMessageTokenCount: node.sourceMessageTokenCount,
|
|
135
|
+
childCount: node.childCount,
|
|
136
|
+
earliestAt: node.earliestAt,
|
|
137
|
+
latestAt: node.latestAt,
|
|
138
|
+
path: node.path,
|
|
139
|
+
costs: {
|
|
140
|
+
summariesOnly: summariesOnlyCost,
|
|
141
|
+
withMessages: withMessagesCost,
|
|
142
|
+
},
|
|
143
|
+
budgetFit: {
|
|
144
|
+
summariesOnly: summariesOnlyCost <= resolvedTokenCap,
|
|
145
|
+
withMessages: withMessagesCost <= resolvedTokenCap,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
|
|
80
150
|
const lines: string[] = [];
|
|
81
|
-
lines.push(
|
|
82
|
-
lines.push(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
151
|
+
lines.push(`LCM_SUMMARY ${id}`);
|
|
152
|
+
lines.push(
|
|
153
|
+
`meta conv=${s.conversationId} kind=${s.kind} depth=${s.depth} tok=${s.tokenCount} ` +
|
|
154
|
+
`descTok=${s.descendantTokenCount} srcTok=${s.sourceMessageTokenCount} ` +
|
|
155
|
+
`desc=${s.descendantCount} range=${formatIso(s.earliestAt)}..${formatIso(s.latestAt)} ` +
|
|
156
|
+
`budgetCap=${resolvedTokenCap}`,
|
|
157
|
+
);
|
|
87
158
|
if (s.parentIds.length > 0) {
|
|
88
|
-
lines.push(
|
|
159
|
+
lines.push(`parents ${s.parentIds.join(" ")}`);
|
|
89
160
|
}
|
|
90
161
|
if (s.childIds.length > 0) {
|
|
91
|
-
lines.push(
|
|
92
|
-
}
|
|
93
|
-
if (s.messageIds.length > 0) {
|
|
94
|
-
lines.push(`**Messages:** ${s.messageIds.length} linked`);
|
|
162
|
+
lines.push(`children ${s.childIds.join(" ")}`);
|
|
95
163
|
}
|
|
96
|
-
|
|
97
|
-
|
|
164
|
+
lines.push("manifest");
|
|
165
|
+
for (const node of manifestNodes) {
|
|
166
|
+
lines.push(
|
|
167
|
+
`d${node.depthFromRoot} ${node.summaryId} k=${node.kind} tok=${node.tokenCount} ` +
|
|
168
|
+
`descTok=${node.descendantTokenCount} srcTok=${node.sourceMessageTokenCount} ` +
|
|
169
|
+
`desc=${node.descendantCount} child=${node.childCount} ` +
|
|
170
|
+
`range=${formatIso(node.earliestAt)}..${formatIso(node.latestAt)} ` +
|
|
171
|
+
`cost[s=${node.costs.summariesOnly},m=${node.costs.withMessages}] ` +
|
|
172
|
+
`budget[s=${node.budgetFit.summariesOnly ? "in" : "over"},` +
|
|
173
|
+
`m=${node.budgetFit.withMessages ? "in" : "over"}]`,
|
|
174
|
+
);
|
|
98
175
|
}
|
|
99
|
-
lines.push("");
|
|
100
|
-
lines.push("## Content");
|
|
101
|
-
lines.push("");
|
|
176
|
+
lines.push("content");
|
|
102
177
|
lines.push(s.content);
|
|
103
178
|
|
|
104
179
|
return {
|
|
105
180
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
106
|
-
details:
|
|
181
|
+
details: {
|
|
182
|
+
...result,
|
|
183
|
+
manifest: {
|
|
184
|
+
tokenCap: resolvedTokenCap,
|
|
185
|
+
budgetSource:
|
|
186
|
+
requestedTokenCap != null
|
|
187
|
+
? "request"
|
|
188
|
+
: typeof delegatedRemainingBudget === "number"
|
|
189
|
+
? "delegated_grant_remaining"
|
|
190
|
+
: "config_default",
|
|
191
|
+
nodes: manifestNodes,
|
|
192
|
+
},
|
|
193
|
+
},
|
|
107
194
|
};
|
|
108
195
|
}
|
|
109
196
|
|
|
@@ -12,6 +12,14 @@ import {
|
|
|
12
12
|
normalizeSummaryIds,
|
|
13
13
|
resolveRequesterConversationScopeId,
|
|
14
14
|
} from "./lcm-expand-tool.delegation.js";
|
|
15
|
+
import {
|
|
16
|
+
clearDelegatedExpansionContext,
|
|
17
|
+
evaluateExpansionRecursionGuard,
|
|
18
|
+
recordExpansionDelegationTelemetry,
|
|
19
|
+
resolveExpansionRequestId,
|
|
20
|
+
resolveNextExpansionDepth,
|
|
21
|
+
stampDelegatedExpansionContext,
|
|
22
|
+
} from "./lcm-expansion-recursion-guard.js";
|
|
15
23
|
|
|
16
24
|
const DELEGATED_WAIT_TIMEOUT_MS = 120_000;
|
|
17
25
|
const GATEWAY_TIMEOUT_MS = 10_000;
|
|
@@ -50,6 +58,13 @@ const LcmExpandQuerySchema = Type.Object({
|
|
|
50
58
|
minimum: 1,
|
|
51
59
|
}),
|
|
52
60
|
),
|
|
61
|
+
tokenCap: Type.Optional(
|
|
62
|
+
Type.Number({
|
|
63
|
+
description:
|
|
64
|
+
"Expansion retrieval token budget across all delegated lcm_expand calls for this query.",
|
|
65
|
+
minimum: 1,
|
|
66
|
+
}),
|
|
67
|
+
),
|
|
53
68
|
});
|
|
54
69
|
|
|
55
70
|
type ExpandQueryReply = {
|
|
@@ -71,23 +86,40 @@ type SummaryCandidate = {
|
|
|
71
86
|
function buildDelegatedExpandQueryTask(params: {
|
|
72
87
|
summaryIds: string[];
|
|
73
88
|
conversationId: number;
|
|
89
|
+
query?: string;
|
|
74
90
|
prompt: string;
|
|
75
91
|
maxTokens: number;
|
|
92
|
+
tokenCap: number;
|
|
93
|
+
requestId: string;
|
|
94
|
+
expansionDepth: number;
|
|
95
|
+
originSessionKey: string;
|
|
76
96
|
}) {
|
|
77
|
-
const
|
|
78
|
-
summaryIds: params.summaryIds,
|
|
79
|
-
conversationId: params.conversationId,
|
|
80
|
-
includeMessages: false,
|
|
81
|
-
};
|
|
97
|
+
const seedSummaryIds = params.summaryIds.length > 0 ? params.summaryIds.join(", ") : "(none)";
|
|
82
98
|
return [
|
|
83
|
-
"
|
|
99
|
+
"You are an autonomous LCM retrieval navigator. Plan and execute retrieval before answering.",
|
|
100
|
+
"",
|
|
101
|
+
"Available tools: lcm_describe, lcm_expand, lcm_grep",
|
|
102
|
+
`Conversation scope: ${params.conversationId}`,
|
|
103
|
+
`Expansion token budget (total across this run): ${params.tokenCap}`,
|
|
104
|
+
`Seed summary IDs: ${seedSummaryIds}`,
|
|
105
|
+
params.query ? `Routing query: ${params.query}` : undefined,
|
|
84
106
|
"",
|
|
85
|
-
"
|
|
86
|
-
|
|
107
|
+
"Strategy:",
|
|
108
|
+
"1. Start with `lcm_describe` on seed summaries to inspect subtree manifests and branch costs.",
|
|
109
|
+
"2. If additional candidates are needed, use `lcm_grep` scoped to summaries.",
|
|
110
|
+
"3. Select branches that fit remaining budget; prefer high-signal paths first.",
|
|
111
|
+
"4. Call `lcm_expand` selectively (do not expand everything blindly).",
|
|
112
|
+
"5. Keep includeMessages=false by default; use includeMessages=true only for specific leaf evidence.",
|
|
113
|
+
`6. Stay within ${params.tokenCap} total expansion tokens across all lcm_expand calls.`,
|
|
87
114
|
"",
|
|
88
|
-
"
|
|
115
|
+
"User prompt to answer:",
|
|
89
116
|
params.prompt,
|
|
90
117
|
"",
|
|
118
|
+
"Delegated expansion metadata (for tracing):",
|
|
119
|
+
`- requestId: ${params.requestId}`,
|
|
120
|
+
`- expansionDepth: ${params.expansionDepth}`,
|
|
121
|
+
`- originSessionKey: ${params.originSessionKey}`,
|
|
122
|
+
"",
|
|
91
123
|
"Return ONLY JSON with this shape:",
|
|
92
124
|
"{",
|
|
93
125
|
' "answer": "string",',
|
|
@@ -98,10 +130,13 @@ function buildDelegatedExpandQueryTask(params: {
|
|
|
98
130
|
"}",
|
|
99
131
|
"",
|
|
100
132
|
"Rules:",
|
|
133
|
+
"- In delegated context, call `lcm_expand` directly for source retrieval.",
|
|
134
|
+
"- DO NOT call `lcm_expand_query` from this delegated session.",
|
|
135
|
+
"- Synthesize the final answer from retrieved evidence, not assumptions.",
|
|
101
136
|
`- Keep answer concise and focused (target <= ${params.maxTokens} tokens).`,
|
|
102
137
|
"- citedIds must be unique summary IDs.",
|
|
103
138
|
"- expandedSummaryCount should reflect how many summaries were expanded/used.",
|
|
104
|
-
"- totalSourceTokens should
|
|
139
|
+
"- totalSourceTokens should estimate total tokens consumed from expansion calls.",
|
|
105
140
|
"- truncated should indicate whether source expansion appears truncated.",
|
|
106
141
|
].join("\n");
|
|
107
142
|
}
|
|
@@ -281,6 +316,11 @@ export function createLcmExpandQueryTool(input: {
|
|
|
281
316
|
typeof requestedMaxTokens === "number" && Number.isFinite(requestedMaxTokens)
|
|
282
317
|
? Math.max(1, requestedMaxTokens)
|
|
283
318
|
: DEFAULT_MAX_ANSWER_TOKENS;
|
|
319
|
+
const requestedTokenCap = typeof p.tokenCap === "number" ? Math.trunc(p.tokenCap) : undefined;
|
|
320
|
+
const expansionTokenCap =
|
|
321
|
+
typeof requestedTokenCap === "number" && Number.isFinite(requestedTokenCap)
|
|
322
|
+
? Math.max(1, requestedTokenCap)
|
|
323
|
+
: Math.max(1, Math.trunc(input.deps.config.maxExpandTokens));
|
|
284
324
|
|
|
285
325
|
if (!prompt) {
|
|
286
326
|
return jsonResult({
|
|
@@ -294,11 +334,46 @@ export function createLcmExpandQueryTool(input: {
|
|
|
294
334
|
});
|
|
295
335
|
}
|
|
296
336
|
|
|
297
|
-
const
|
|
337
|
+
const callerSessionKey =
|
|
298
338
|
(typeof input.requesterSessionKey === "string"
|
|
299
339
|
? input.requesterSessionKey
|
|
300
340
|
: input.sessionId
|
|
301
341
|
)?.trim() ?? "";
|
|
342
|
+
const requestId = resolveExpansionRequestId(callerSessionKey);
|
|
343
|
+
const recursionCheck = evaluateExpansionRecursionGuard({
|
|
344
|
+
sessionKey: callerSessionKey,
|
|
345
|
+
requestId,
|
|
346
|
+
});
|
|
347
|
+
recordExpansionDelegationTelemetry({
|
|
348
|
+
deps: input.deps,
|
|
349
|
+
component: "lcm_expand_query",
|
|
350
|
+
event: "start",
|
|
351
|
+
requestId,
|
|
352
|
+
sessionKey: callerSessionKey,
|
|
353
|
+
expansionDepth: recursionCheck.expansionDepth,
|
|
354
|
+
originSessionKey: recursionCheck.originSessionKey,
|
|
355
|
+
});
|
|
356
|
+
if (recursionCheck.blocked) {
|
|
357
|
+
recordExpansionDelegationTelemetry({
|
|
358
|
+
deps: input.deps,
|
|
359
|
+
component: "lcm_expand_query",
|
|
360
|
+
event: "block",
|
|
361
|
+
requestId,
|
|
362
|
+
sessionKey: callerSessionKey,
|
|
363
|
+
expansionDepth: recursionCheck.expansionDepth,
|
|
364
|
+
originSessionKey: recursionCheck.originSessionKey,
|
|
365
|
+
reason: recursionCheck.reason,
|
|
366
|
+
});
|
|
367
|
+
return jsonResult({
|
|
368
|
+
errorCode: recursionCheck.code,
|
|
369
|
+
error: recursionCheck.message,
|
|
370
|
+
requestId: recursionCheck.requestId,
|
|
371
|
+
expansionDepth: recursionCheck.expansionDepth,
|
|
372
|
+
originSessionKey: recursionCheck.originSessionKey,
|
|
373
|
+
reason: recursionCheck.reason,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
302
377
|
const conversationScope = await resolveLcmConversationScope({
|
|
303
378
|
lcm: input.lcm,
|
|
304
379
|
deps: input.deps,
|
|
@@ -310,11 +385,11 @@ export function createLcmExpandQueryTool(input: {
|
|
|
310
385
|
if (
|
|
311
386
|
!conversationScope.allConversations &&
|
|
312
387
|
scopedConversationId == null &&
|
|
313
|
-
|
|
388
|
+
callerSessionKey
|
|
314
389
|
) {
|
|
315
390
|
scopedConversationId = await resolveRequesterConversationScopeId({
|
|
316
391
|
deps: input.deps,
|
|
317
|
-
requesterSessionKey,
|
|
392
|
+
requesterSessionKey: callerSessionKey,
|
|
318
393
|
lcm: input.lcm,
|
|
319
394
|
});
|
|
320
395
|
}
|
|
@@ -371,24 +446,38 @@ export function createLcmExpandQueryTool(input: {
|
|
|
371
446
|
}
|
|
372
447
|
|
|
373
448
|
const requesterAgentId = input.deps.normalizeAgentId(
|
|
374
|
-
input.deps.parseAgentSessionKey(
|
|
449
|
+
input.deps.parseAgentSessionKey(callerSessionKey)?.agentId,
|
|
375
450
|
);
|
|
376
451
|
childSessionKey = `agent:${requesterAgentId}:subagent:${crypto.randomUUID()}`;
|
|
452
|
+
const childExpansionDepth = resolveNextExpansionDepth(callerSessionKey);
|
|
453
|
+
const originSessionKey = recursionCheck.originSessionKey || callerSessionKey || "main";
|
|
377
454
|
|
|
378
455
|
createDelegatedExpansionGrant({
|
|
379
456
|
delegatedSessionKey: childSessionKey,
|
|
380
|
-
issuerSessionId:
|
|
457
|
+
issuerSessionId: callerSessionKey || "main",
|
|
381
458
|
allowedConversationIds: [sourceConversationId],
|
|
382
|
-
tokenCap:
|
|
459
|
+
tokenCap: expansionTokenCap,
|
|
383
460
|
ttlMs: DELEGATED_WAIT_TIMEOUT_MS + 30_000,
|
|
384
461
|
});
|
|
462
|
+
stampDelegatedExpansionContext({
|
|
463
|
+
sessionKey: childSessionKey,
|
|
464
|
+
requestId,
|
|
465
|
+
expansionDepth: childExpansionDepth,
|
|
466
|
+
originSessionKey,
|
|
467
|
+
stampedBy: "lcm_expand_query",
|
|
468
|
+
});
|
|
385
469
|
grantCreated = true;
|
|
386
470
|
|
|
387
471
|
const task = buildDelegatedExpandQueryTask({
|
|
388
472
|
summaryIds,
|
|
389
473
|
conversationId: sourceConversationId,
|
|
474
|
+
query: query || undefined,
|
|
390
475
|
prompt,
|
|
391
476
|
maxTokens,
|
|
477
|
+
tokenCap: expansionTokenCap,
|
|
478
|
+
requestId,
|
|
479
|
+
expansionDepth: childExpansionDepth,
|
|
480
|
+
originSessionKey,
|
|
392
481
|
});
|
|
393
482
|
|
|
394
483
|
const childIdem = crypto.randomUUID();
|
|
@@ -426,6 +515,16 @@ export function createLcmExpandQueryTool(input: {
|
|
|
426
515
|
})) as { status?: string; error?: string };
|
|
427
516
|
const status = typeof wait?.status === "string" ? wait.status : "error";
|
|
428
517
|
if (status === "timeout") {
|
|
518
|
+
recordExpansionDelegationTelemetry({
|
|
519
|
+
deps: input.deps,
|
|
520
|
+
component: "lcm_expand_query",
|
|
521
|
+
event: "timeout",
|
|
522
|
+
requestId,
|
|
523
|
+
sessionKey: callerSessionKey,
|
|
524
|
+
expansionDepth: childExpansionDepth,
|
|
525
|
+
originSessionKey,
|
|
526
|
+
runId,
|
|
527
|
+
});
|
|
429
528
|
return jsonResult({
|
|
430
529
|
error: "lcm_expand_query timed out waiting for delegated expansion (120s).",
|
|
431
530
|
});
|
|
@@ -448,6 +547,16 @@ export function createLcmExpandQueryTool(input: {
|
|
|
448
547
|
Array.isArray(replyPayload.messages) ? replyPayload.messages : [],
|
|
449
548
|
);
|
|
450
549
|
const parsed = parseDelegatedExpandQueryReply(reply, summaryIds.length);
|
|
550
|
+
recordExpansionDelegationTelemetry({
|
|
551
|
+
deps: input.deps,
|
|
552
|
+
component: "lcm_expand_query",
|
|
553
|
+
event: "success",
|
|
554
|
+
requestId,
|
|
555
|
+
sessionKey: callerSessionKey,
|
|
556
|
+
expansionDepth: childExpansionDepth,
|
|
557
|
+
originSessionKey,
|
|
558
|
+
runId,
|
|
559
|
+
});
|
|
451
560
|
|
|
452
561
|
return jsonResult({
|
|
453
562
|
answer: parsed.answer,
|
|
@@ -476,6 +585,9 @@ export function createLcmExpandQueryTool(input: {
|
|
|
476
585
|
if (grantCreated && childSessionKey) {
|
|
477
586
|
revokeDelegatedExpansionGrantForSession(childSessionKey, { removeBinding: true });
|
|
478
587
|
}
|
|
588
|
+
if (childSessionKey) {
|
|
589
|
+
clearDelegatedExpansionContext(childSessionKey);
|
|
590
|
+
}
|
|
479
591
|
}
|
|
480
592
|
},
|
|
481
593
|
};
|
|
@@ -5,6 +5,13 @@ import {
|
|
|
5
5
|
revokeDelegatedExpansionGrantForSession,
|
|
6
6
|
} from "../expansion-auth.js";
|
|
7
7
|
import type { LcmDependencies } from "../types.js";
|
|
8
|
+
import {
|
|
9
|
+
clearDelegatedExpansionContext,
|
|
10
|
+
evaluateExpansionRecursionGuard,
|
|
11
|
+
recordExpansionDelegationTelemetry,
|
|
12
|
+
resolveExpansionRequestId,
|
|
13
|
+
stampDelegatedExpansionContext,
|
|
14
|
+
} from "./lcm-expansion-recursion-guard.js";
|
|
8
15
|
|
|
9
16
|
const MAX_GATEWAY_TIMEOUT_MS = 2_147_483_647;
|
|
10
17
|
|
|
@@ -156,6 +163,9 @@ function buildDelegatedExpansionTask(params: {
|
|
|
156
163
|
includeMessages: boolean;
|
|
157
164
|
pass: number;
|
|
158
165
|
query?: string;
|
|
166
|
+
requestId: string;
|
|
167
|
+
expansionDepth: number;
|
|
168
|
+
originSessionKey: string;
|
|
159
169
|
}) {
|
|
160
170
|
const payload: {
|
|
161
171
|
summaryIds: string[];
|
|
@@ -180,6 +190,11 @@ function buildDelegatedExpansionTask(params: {
|
|
|
180
190
|
"Call `lcm_expand` using exactly this JSON payload:",
|
|
181
191
|
JSON.stringify(payload, null, 2),
|
|
182
192
|
"",
|
|
193
|
+
"Delegated expansion metadata (for tracing):",
|
|
194
|
+
`- requestId: ${params.requestId}`,
|
|
195
|
+
`- expansionDepth: ${params.expansionDepth}`,
|
|
196
|
+
`- originSessionKey: ${params.originSessionKey}`,
|
|
197
|
+
"",
|
|
183
198
|
"Then return ONLY JSON with this shape:",
|
|
184
199
|
"{",
|
|
185
200
|
' "summary": "string concise findings",',
|
|
@@ -190,7 +205,10 @@ function buildDelegatedExpansionTask(params: {
|
|
|
190
205
|
"}",
|
|
191
206
|
"",
|
|
192
207
|
"Rules:",
|
|
208
|
+
"- In delegated context, use `lcm_expand` directly for retrieval.",
|
|
209
|
+
"- DO NOT call `lcm_expand_query` from this delegated session.",
|
|
193
210
|
"- Keep summary concise and factual.",
|
|
211
|
+
"- Synthesize findings from the `lcm_expand` result before returning.",
|
|
194
212
|
"- citedIds/followUpSummaryIds must contain unique summary IDs only.",
|
|
195
213
|
"- If no follow-up is needed, return an empty followUpSummaryIds array.",
|
|
196
214
|
]
|
|
@@ -240,6 +258,7 @@ async function runDelegatedExpansionPass(params: {
|
|
|
240
258
|
| "buildSubagentSystemPrompt"
|
|
241
259
|
| "readLatestAssistantReply"
|
|
242
260
|
| "agentLaneSubagent"
|
|
261
|
+
| "log"
|
|
243
262
|
>;
|
|
244
263
|
requesterSessionKey: string;
|
|
245
264
|
conversationId: number;
|
|
@@ -249,6 +268,9 @@ async function runDelegatedExpansionPass(params: {
|
|
|
249
268
|
includeMessages: boolean;
|
|
250
269
|
query?: string;
|
|
251
270
|
pass: number;
|
|
271
|
+
requestId: string;
|
|
272
|
+
parentExpansionDepth: number;
|
|
273
|
+
originSessionKey: string;
|
|
252
274
|
}): Promise<DelegatedExpansionPassResult> {
|
|
253
275
|
const requesterAgentId = params.deps.normalizeAgentId(
|
|
254
276
|
params.deps.parseAgentSessionKey(params.requesterSessionKey)?.agentId,
|
|
@@ -260,8 +282,16 @@ async function runDelegatedExpansionPass(params: {
|
|
|
260
282
|
delegatedSessionKey: childSessionKey,
|
|
261
283
|
issuerSessionId: params.requesterSessionKey,
|
|
262
284
|
allowedConversationIds: [params.conversationId],
|
|
285
|
+
tokenCap: params.tokenCap,
|
|
263
286
|
ttlMs: MAX_GATEWAY_TIMEOUT_MS,
|
|
264
287
|
});
|
|
288
|
+
stampDelegatedExpansionContext({
|
|
289
|
+
sessionKey: childSessionKey,
|
|
290
|
+
requestId: params.requestId,
|
|
291
|
+
expansionDepth: params.parentExpansionDepth + 1,
|
|
292
|
+
originSessionKey: params.originSessionKey,
|
|
293
|
+
stampedBy: "runDelegatedExpansionLoop",
|
|
294
|
+
});
|
|
265
295
|
|
|
266
296
|
try {
|
|
267
297
|
const message = buildDelegatedExpansionTask({
|
|
@@ -272,6 +302,9 @@ async function runDelegatedExpansionPass(params: {
|
|
|
272
302
|
includeMessages: params.includeMessages,
|
|
273
303
|
pass: params.pass,
|
|
274
304
|
query: params.query,
|
|
305
|
+
requestId: params.requestId,
|
|
306
|
+
expansionDepth: params.parentExpansionDepth + 1,
|
|
307
|
+
originSessionKey: params.originSessionKey,
|
|
275
308
|
});
|
|
276
309
|
const response = (await params.deps.callGateway({
|
|
277
310
|
method: "agent",
|
|
@@ -374,6 +407,7 @@ async function runDelegatedExpansionPass(params: {
|
|
|
374
407
|
// Cleanup is best-effort.
|
|
375
408
|
}
|
|
376
409
|
revokeDelegatedExpansionGrantForSession(childSessionKey, { removeBinding: true });
|
|
410
|
+
clearDelegatedExpansionContext(childSessionKey);
|
|
377
411
|
}
|
|
378
412
|
}
|
|
379
413
|
|
|
@@ -386,6 +420,7 @@ export async function runDelegatedExpansionLoop(params: {
|
|
|
386
420
|
| "buildSubagentSystemPrompt"
|
|
387
421
|
| "readLatestAssistantReply"
|
|
388
422
|
| "agentLaneSubagent"
|
|
423
|
+
| "log"
|
|
389
424
|
>;
|
|
390
425
|
requesterSessionKey: string;
|
|
391
426
|
conversationId: number;
|
|
@@ -394,7 +429,44 @@ export async function runDelegatedExpansionLoop(params: {
|
|
|
394
429
|
tokenCap?: number;
|
|
395
430
|
includeMessages: boolean;
|
|
396
431
|
query?: string;
|
|
432
|
+
requestId?: string;
|
|
397
433
|
}): Promise<DelegatedExpansionLoopResult> {
|
|
434
|
+
const requestId = params.requestId?.trim() || resolveExpansionRequestId(params.requesterSessionKey);
|
|
435
|
+
const recursionCheck = evaluateExpansionRecursionGuard({
|
|
436
|
+
sessionKey: params.requesterSessionKey,
|
|
437
|
+
requestId,
|
|
438
|
+
});
|
|
439
|
+
recordExpansionDelegationTelemetry({
|
|
440
|
+
deps: params.deps,
|
|
441
|
+
component: "runDelegatedExpansionLoop",
|
|
442
|
+
event: "start",
|
|
443
|
+
requestId,
|
|
444
|
+
sessionKey: params.requesterSessionKey,
|
|
445
|
+
expansionDepth: recursionCheck.expansionDepth,
|
|
446
|
+
originSessionKey: recursionCheck.originSessionKey,
|
|
447
|
+
});
|
|
448
|
+
if (recursionCheck.blocked) {
|
|
449
|
+
recordExpansionDelegationTelemetry({
|
|
450
|
+
deps: params.deps,
|
|
451
|
+
component: "runDelegatedExpansionLoop",
|
|
452
|
+
event: "block",
|
|
453
|
+
requestId,
|
|
454
|
+
sessionKey: params.requesterSessionKey,
|
|
455
|
+
expansionDepth: recursionCheck.expansionDepth,
|
|
456
|
+
originSessionKey: recursionCheck.originSessionKey,
|
|
457
|
+
reason: recursionCheck.reason,
|
|
458
|
+
});
|
|
459
|
+
return {
|
|
460
|
+
status: "error",
|
|
461
|
+
passes: [],
|
|
462
|
+
citedIds: [],
|
|
463
|
+
totalTokens: 0,
|
|
464
|
+
truncated: true,
|
|
465
|
+
text: "Delegated expansion blocked by recursion guard.",
|
|
466
|
+
error: recursionCheck.message,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
398
470
|
const passes: DelegatedExpansionPassResult[] = [];
|
|
399
471
|
const visited = new Set<string>();
|
|
400
472
|
const cited = new Set<string>();
|
|
@@ -415,10 +487,25 @@ export async function runDelegatedExpansionLoop(params: {
|
|
|
415
487
|
includeMessages: params.includeMessages,
|
|
416
488
|
query: params.query,
|
|
417
489
|
pass,
|
|
490
|
+
requestId,
|
|
491
|
+
parentExpansionDepth: recursionCheck.expansionDepth,
|
|
492
|
+
originSessionKey: recursionCheck.originSessionKey,
|
|
418
493
|
});
|
|
419
494
|
passes.push(result);
|
|
420
495
|
|
|
421
496
|
if (result.status !== "ok") {
|
|
497
|
+
if (result.status === "timeout") {
|
|
498
|
+
recordExpansionDelegationTelemetry({
|
|
499
|
+
deps: params.deps,
|
|
500
|
+
component: "runDelegatedExpansionLoop",
|
|
501
|
+
event: "timeout",
|
|
502
|
+
requestId,
|
|
503
|
+
sessionKey: params.requesterSessionKey,
|
|
504
|
+
expansionDepth: recursionCheck.expansionDepth,
|
|
505
|
+
originSessionKey: recursionCheck.originSessionKey,
|
|
506
|
+
runId: result.runId,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
422
509
|
const okPasses = passes.filter((entry) => entry.status === "ok");
|
|
423
510
|
for (const okPass of okPasses) {
|
|
424
511
|
for (const summaryId of okPass.citedIds) {
|
|
@@ -449,6 +536,15 @@ export async function runDelegatedExpansionLoop(params: {
|
|
|
449
536
|
pass += 1;
|
|
450
537
|
}
|
|
451
538
|
|
|
539
|
+
recordExpansionDelegationTelemetry({
|
|
540
|
+
deps: params.deps,
|
|
541
|
+
component: "runDelegatedExpansionLoop",
|
|
542
|
+
event: "success",
|
|
543
|
+
requestId,
|
|
544
|
+
sessionKey: params.requesterSessionKey,
|
|
545
|
+
expansionDepth: recursionCheck.expansionDepth,
|
|
546
|
+
originSessionKey: recursionCheck.originSessionKey,
|
|
547
|
+
});
|
|
452
548
|
return {
|
|
453
549
|
status: "ok",
|
|
454
550
|
passes,
|