@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
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import type { LcmContextEngine } from "../engine.js";
|
|
4
|
+
import {
|
|
5
|
+
createDelegatedExpansionGrant,
|
|
6
|
+
revokeDelegatedExpansionGrantForSession,
|
|
7
|
+
} from "../expansion-auth.js";
|
|
8
|
+
import type { LcmDependencies } from "../types.js";
|
|
9
|
+
import { jsonResult, type AnyAgentTool } from "./common.js";
|
|
10
|
+
import { resolveLcmConversationScope } from "./lcm-conversation-scope.js";
|
|
11
|
+
import {
|
|
12
|
+
normalizeSummaryIds,
|
|
13
|
+
resolveRequesterConversationScopeId,
|
|
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";
|
|
23
|
+
|
|
24
|
+
const DELEGATED_WAIT_TIMEOUT_MS = 120_000;
|
|
25
|
+
const GATEWAY_TIMEOUT_MS = 10_000;
|
|
26
|
+
const DEFAULT_MAX_ANSWER_TOKENS = 2_000;
|
|
27
|
+
|
|
28
|
+
const LcmExpandQuerySchema = Type.Object({
|
|
29
|
+
summaryIds: Type.Optional(
|
|
30
|
+
Type.Array(Type.String(), {
|
|
31
|
+
description: "Summary IDs to expand (sum_xxx). Required when query is not provided.",
|
|
32
|
+
}),
|
|
33
|
+
),
|
|
34
|
+
query: Type.Optional(
|
|
35
|
+
Type.String({
|
|
36
|
+
description:
|
|
37
|
+
"Text query used to find summaries via grep before expansion. Required when summaryIds is not provided.",
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
prompt: Type.String({
|
|
41
|
+
description: "Question to answer using expanded context.",
|
|
42
|
+
}),
|
|
43
|
+
conversationId: Type.Optional(
|
|
44
|
+
Type.Number({
|
|
45
|
+
description:
|
|
46
|
+
"Conversation ID to scope expansion to. If omitted, uses the current session conversation.",
|
|
47
|
+
}),
|
|
48
|
+
),
|
|
49
|
+
allConversations: Type.Optional(
|
|
50
|
+
Type.Boolean({
|
|
51
|
+
description:
|
|
52
|
+
"Set true to explicitly allow cross-conversation lookup. Ignored when conversationId is provided.",
|
|
53
|
+
}),
|
|
54
|
+
),
|
|
55
|
+
maxTokens: Type.Optional(
|
|
56
|
+
Type.Number({
|
|
57
|
+
description: `Maximum answer tokens to target (default: ${DEFAULT_MAX_ANSWER_TOKENS}).`,
|
|
58
|
+
minimum: 1,
|
|
59
|
+
}),
|
|
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
|
+
),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
type ExpandQueryReply = {
|
|
71
|
+
answer: string;
|
|
72
|
+
citedIds: string[];
|
|
73
|
+
expandedSummaryCount: number;
|
|
74
|
+
totalSourceTokens: number;
|
|
75
|
+
truncated: boolean;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
type SummaryCandidate = {
|
|
79
|
+
summaryId: string;
|
|
80
|
+
conversationId: number;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build the sub-agent task message for delegated expansion and prompt answering.
|
|
85
|
+
*/
|
|
86
|
+
function buildDelegatedExpandQueryTask(params: {
|
|
87
|
+
summaryIds: string[];
|
|
88
|
+
conversationId: number;
|
|
89
|
+
query?: string;
|
|
90
|
+
prompt: string;
|
|
91
|
+
maxTokens: number;
|
|
92
|
+
tokenCap: number;
|
|
93
|
+
requestId: string;
|
|
94
|
+
expansionDepth: number;
|
|
95
|
+
originSessionKey: string;
|
|
96
|
+
}) {
|
|
97
|
+
const seedSummaryIds = params.summaryIds.length > 0 ? params.summaryIds.join(", ") : "(none)";
|
|
98
|
+
return [
|
|
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,
|
|
106
|
+
"",
|
|
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.`,
|
|
114
|
+
"",
|
|
115
|
+
"User prompt to answer:",
|
|
116
|
+
params.prompt,
|
|
117
|
+
"",
|
|
118
|
+
"Delegated expansion metadata (for tracing):",
|
|
119
|
+
`- requestId: ${params.requestId}`,
|
|
120
|
+
`- expansionDepth: ${params.expansionDepth}`,
|
|
121
|
+
`- originSessionKey: ${params.originSessionKey}`,
|
|
122
|
+
"",
|
|
123
|
+
"Return ONLY JSON with this shape:",
|
|
124
|
+
"{",
|
|
125
|
+
' "answer": "string",',
|
|
126
|
+
' "citedIds": ["sum_xxx"],',
|
|
127
|
+
' "expandedSummaryCount": 0,',
|
|
128
|
+
' "totalSourceTokens": 0,',
|
|
129
|
+
' "truncated": false',
|
|
130
|
+
"}",
|
|
131
|
+
"",
|
|
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.",
|
|
136
|
+
`- Keep answer concise and focused (target <= ${params.maxTokens} tokens).`,
|
|
137
|
+
"- citedIds must be unique summary IDs.",
|
|
138
|
+
"- expandedSummaryCount should reflect how many summaries were expanded/used.",
|
|
139
|
+
"- totalSourceTokens should estimate total tokens consumed from expansion calls.",
|
|
140
|
+
"- truncated should indicate whether source expansion appears truncated.",
|
|
141
|
+
].join("\n");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Parse the child reply; accepts plain JSON or fenced JSON.
|
|
146
|
+
*/
|
|
147
|
+
function parseDelegatedExpandQueryReply(
|
|
148
|
+
rawReply: string | undefined,
|
|
149
|
+
fallbackExpandedSummaryCount: number,
|
|
150
|
+
): ExpandQueryReply {
|
|
151
|
+
const fallback: ExpandQueryReply = {
|
|
152
|
+
answer: (rawReply ?? "").trim(),
|
|
153
|
+
citedIds: [],
|
|
154
|
+
expandedSummaryCount: fallbackExpandedSummaryCount,
|
|
155
|
+
totalSourceTokens: 0,
|
|
156
|
+
truncated: false,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const reply = rawReply?.trim();
|
|
160
|
+
if (!reply) {
|
|
161
|
+
return fallback;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const candidates: string[] = [reply];
|
|
165
|
+
const fenced = reply.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
166
|
+
if (fenced?.[1]) {
|
|
167
|
+
candidates.unshift(fenced[1].trim());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const candidate of candidates) {
|
|
171
|
+
try {
|
|
172
|
+
const parsed = JSON.parse(candidate) as {
|
|
173
|
+
answer?: unknown;
|
|
174
|
+
citedIds?: unknown;
|
|
175
|
+
expandedSummaryCount?: unknown;
|
|
176
|
+
totalSourceTokens?: unknown;
|
|
177
|
+
truncated?: unknown;
|
|
178
|
+
};
|
|
179
|
+
const answer = typeof parsed.answer === "string" ? parsed.answer.trim() : "";
|
|
180
|
+
const citedIds = normalizeSummaryIds(
|
|
181
|
+
Array.isArray(parsed.citedIds)
|
|
182
|
+
? parsed.citedIds.filter((value): value is string => typeof value === "string")
|
|
183
|
+
: undefined,
|
|
184
|
+
);
|
|
185
|
+
const expandedSummaryCount =
|
|
186
|
+
typeof parsed.expandedSummaryCount === "number" &&
|
|
187
|
+
Number.isFinite(parsed.expandedSummaryCount)
|
|
188
|
+
? Math.max(0, Math.floor(parsed.expandedSummaryCount))
|
|
189
|
+
: fallbackExpandedSummaryCount;
|
|
190
|
+
const totalSourceTokens =
|
|
191
|
+
typeof parsed.totalSourceTokens === "number" && Number.isFinite(parsed.totalSourceTokens)
|
|
192
|
+
? Math.max(0, Math.floor(parsed.totalSourceTokens))
|
|
193
|
+
: 0;
|
|
194
|
+
const truncated = parsed.truncated === true;
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
answer: answer || fallback.answer,
|
|
198
|
+
citedIds,
|
|
199
|
+
expandedSummaryCount,
|
|
200
|
+
totalSourceTokens,
|
|
201
|
+
truncated,
|
|
202
|
+
};
|
|
203
|
+
} catch {
|
|
204
|
+
// Try next candidate.
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return fallback;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Resolve a single source conversation for delegated expansion.
|
|
213
|
+
*/
|
|
214
|
+
function resolveSourceConversationId(params: {
|
|
215
|
+
scopedConversationId?: number;
|
|
216
|
+
allConversations: boolean;
|
|
217
|
+
candidates: SummaryCandidate[];
|
|
218
|
+
}): number {
|
|
219
|
+
if (typeof params.scopedConversationId === "number") {
|
|
220
|
+
const mismatched = params.candidates
|
|
221
|
+
.filter((candidate) => candidate.conversationId !== params.scopedConversationId)
|
|
222
|
+
.map((candidate) => candidate.summaryId);
|
|
223
|
+
if (mismatched.length > 0) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`Some summaryIds are outside conversation ${params.scopedConversationId}: ${mismatched.join(", ")}`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
return params.scopedConversationId;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const conversationIds = Array.from(
|
|
232
|
+
new Set(params.candidates.map((candidate) => candidate.conversationId)),
|
|
233
|
+
);
|
|
234
|
+
if (conversationIds.length === 1 && typeof conversationIds[0] === "number") {
|
|
235
|
+
return conversationIds[0];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (params.allConversations && conversationIds.length > 1) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
"Query matched summaries from multiple conversations. Provide conversationId or narrow the query.",
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
throw new Error(
|
|
245
|
+
"Unable to resolve a single conversation scope. Provide conversationId or set a narrower summary scope.",
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Resolve summary candidates from explicit IDs and/or query matches.
|
|
251
|
+
*/
|
|
252
|
+
async function resolveSummaryCandidates(params: {
|
|
253
|
+
lcm: LcmContextEngine;
|
|
254
|
+
explicitSummaryIds: string[];
|
|
255
|
+
query?: string;
|
|
256
|
+
conversationId?: number;
|
|
257
|
+
}): Promise<SummaryCandidate[]> {
|
|
258
|
+
const retrieval = params.lcm.getRetrieval();
|
|
259
|
+
const candidates = new Map<string, SummaryCandidate>();
|
|
260
|
+
|
|
261
|
+
for (const summaryId of params.explicitSummaryIds) {
|
|
262
|
+
const described = await retrieval.describe(summaryId);
|
|
263
|
+
if (!described || described.type !== "summary" || !described.summary) {
|
|
264
|
+
throw new Error(`Summary not found: ${summaryId}`);
|
|
265
|
+
}
|
|
266
|
+
candidates.set(summaryId, {
|
|
267
|
+
summaryId,
|
|
268
|
+
conversationId: described.summary.conversationId,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (params.query) {
|
|
273
|
+
const grepResult = await retrieval.grep({
|
|
274
|
+
query: params.query,
|
|
275
|
+
mode: "full_text",
|
|
276
|
+
scope: "summaries",
|
|
277
|
+
conversationId: params.conversationId,
|
|
278
|
+
});
|
|
279
|
+
for (const summary of grepResult.summaries) {
|
|
280
|
+
candidates.set(summary.summaryId, {
|
|
281
|
+
summaryId: summary.summaryId,
|
|
282
|
+
conversationId: summary.conversationId,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return Array.from(candidates.values());
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function createLcmExpandQueryTool(input: {
|
|
291
|
+
deps: LcmDependencies;
|
|
292
|
+
lcm: LcmContextEngine;
|
|
293
|
+
/** Session id used for LCM conversation scoping. */
|
|
294
|
+
sessionId?: string;
|
|
295
|
+
/** Requester agent session key used for delegated child session/auth scoping. */
|
|
296
|
+
requesterSessionKey?: string;
|
|
297
|
+
/** Session key for scope fallback when sessionId is unavailable. */
|
|
298
|
+
sessionKey?: string;
|
|
299
|
+
}): AnyAgentTool {
|
|
300
|
+
return {
|
|
301
|
+
name: "lcm_expand_query",
|
|
302
|
+
label: "LCM Expand Query",
|
|
303
|
+
description:
|
|
304
|
+
"Answer a focused question using delegated LCM expansion. " +
|
|
305
|
+
"Find candidate summaries (by IDs or query), expand them in a delegated sub-agent, " +
|
|
306
|
+
"and return a compact prompt-focused answer with cited summary IDs.",
|
|
307
|
+
parameters: LcmExpandQuerySchema,
|
|
308
|
+
async execute(_toolCallId, params) {
|
|
309
|
+
const p = params as Record<string, unknown>;
|
|
310
|
+
const explicitSummaryIds = normalizeSummaryIds(p.summaryIds as string[] | undefined);
|
|
311
|
+
const query = typeof p.query === "string" ? p.query.trim() : "";
|
|
312
|
+
const prompt = typeof p.prompt === "string" ? p.prompt.trim() : "";
|
|
313
|
+
const requestedMaxTokens =
|
|
314
|
+
typeof p.maxTokens === "number" ? Math.trunc(p.maxTokens) : undefined;
|
|
315
|
+
const maxTokens =
|
|
316
|
+
typeof requestedMaxTokens === "number" && Number.isFinite(requestedMaxTokens)
|
|
317
|
+
? Math.max(1, requestedMaxTokens)
|
|
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));
|
|
324
|
+
|
|
325
|
+
if (!prompt) {
|
|
326
|
+
return jsonResult({
|
|
327
|
+
error: "prompt is required.",
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (explicitSummaryIds.length === 0 && !query) {
|
|
332
|
+
return jsonResult({
|
|
333
|
+
error: "Either summaryIds or query must be provided.",
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const callerSessionKey =
|
|
338
|
+
(typeof input.requesterSessionKey === "string"
|
|
339
|
+
? input.requesterSessionKey
|
|
340
|
+
: input.sessionId
|
|
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
|
+
|
|
377
|
+
const conversationScope = await resolveLcmConversationScope({
|
|
378
|
+
lcm: input.lcm,
|
|
379
|
+
deps: input.deps,
|
|
380
|
+
sessionId: input.sessionId,
|
|
381
|
+
sessionKey: input.sessionKey,
|
|
382
|
+
params: p,
|
|
383
|
+
});
|
|
384
|
+
let scopedConversationId = conversationScope.conversationId;
|
|
385
|
+
if (
|
|
386
|
+
!conversationScope.allConversations &&
|
|
387
|
+
scopedConversationId == null &&
|
|
388
|
+
callerSessionKey
|
|
389
|
+
) {
|
|
390
|
+
scopedConversationId = await resolveRequesterConversationScopeId({
|
|
391
|
+
deps: input.deps,
|
|
392
|
+
requesterSessionKey: callerSessionKey,
|
|
393
|
+
lcm: input.lcm,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!conversationScope.allConversations && scopedConversationId == null) {
|
|
398
|
+
return jsonResult({
|
|
399
|
+
error:
|
|
400
|
+
"No LCM conversation found for this session. Provide conversationId or set allConversations=true.",
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let childSessionKey = "";
|
|
405
|
+
let grantCreated = false;
|
|
406
|
+
|
|
407
|
+
try {
|
|
408
|
+
const candidates = await resolveSummaryCandidates({
|
|
409
|
+
lcm: input.lcm,
|
|
410
|
+
explicitSummaryIds,
|
|
411
|
+
query: query || undefined,
|
|
412
|
+
conversationId: scopedConversationId,
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
if (candidates.length === 0) {
|
|
416
|
+
if (typeof scopedConversationId !== "number") {
|
|
417
|
+
return jsonResult({
|
|
418
|
+
error: "No matching summaries found.",
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
return jsonResult({
|
|
422
|
+
answer: "No matching summaries found for this scope.",
|
|
423
|
+
citedIds: [],
|
|
424
|
+
sourceConversationId: scopedConversationId,
|
|
425
|
+
expandedSummaryCount: 0,
|
|
426
|
+
totalSourceTokens: 0,
|
|
427
|
+
truncated: false,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const sourceConversationId = resolveSourceConversationId({
|
|
432
|
+
scopedConversationId,
|
|
433
|
+
allConversations: conversationScope.allConversations,
|
|
434
|
+
candidates,
|
|
435
|
+
});
|
|
436
|
+
const summaryIds = normalizeSummaryIds(
|
|
437
|
+
candidates
|
|
438
|
+
.filter((candidate) => candidate.conversationId === sourceConversationId)
|
|
439
|
+
.map((candidate) => candidate.summaryId),
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
if (summaryIds.length === 0) {
|
|
443
|
+
return jsonResult({
|
|
444
|
+
error: "No summaryIds available after applying conversation scope.",
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const requesterAgentId = input.deps.normalizeAgentId(
|
|
449
|
+
input.deps.parseAgentSessionKey(callerSessionKey)?.agentId,
|
|
450
|
+
);
|
|
451
|
+
childSessionKey = `agent:${requesterAgentId}:subagent:${crypto.randomUUID()}`;
|
|
452
|
+
const childExpansionDepth = resolveNextExpansionDepth(callerSessionKey);
|
|
453
|
+
const originSessionKey = recursionCheck.originSessionKey || callerSessionKey || "main";
|
|
454
|
+
|
|
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
|
+
const task = buildDelegatedExpandQueryTask({
|
|
472
|
+
summaryIds,
|
|
473
|
+
conversationId: sourceConversationId,
|
|
474
|
+
query: query || undefined,
|
|
475
|
+
prompt,
|
|
476
|
+
maxTokens,
|
|
477
|
+
tokenCap: expansionTokenCap,
|
|
478
|
+
requestId,
|
|
479
|
+
expansionDepth: childExpansionDepth,
|
|
480
|
+
originSessionKey,
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const childIdem = crypto.randomUUID();
|
|
484
|
+
const response = (await input.deps.callGateway({
|
|
485
|
+
method: "agent",
|
|
486
|
+
params: {
|
|
487
|
+
message: task,
|
|
488
|
+
sessionKey: childSessionKey,
|
|
489
|
+
deliver: false,
|
|
490
|
+
lane: input.deps.agentLaneSubagent,
|
|
491
|
+
idempotencyKey: childIdem,
|
|
492
|
+
extraSystemPrompt: input.deps.buildSubagentSystemPrompt({
|
|
493
|
+
depth: 1,
|
|
494
|
+
maxDepth: 8,
|
|
495
|
+
taskSummary: "Run lcm_expand and return prompt-focused JSON answer",
|
|
496
|
+
}),
|
|
497
|
+
},
|
|
498
|
+
timeoutMs: GATEWAY_TIMEOUT_MS,
|
|
499
|
+
})) as { runId?: string };
|
|
500
|
+
|
|
501
|
+
const runId = typeof response?.runId === "string" ? response.runId.trim() : "";
|
|
502
|
+
if (!runId) {
|
|
503
|
+
return jsonResult({
|
|
504
|
+
error: "Delegated expansion did not return a runId.",
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const wait = (await input.deps.callGateway({
|
|
509
|
+
method: "agent.wait",
|
|
510
|
+
params: {
|
|
511
|
+
runId,
|
|
512
|
+
timeoutMs: DELEGATED_WAIT_TIMEOUT_MS,
|
|
513
|
+
},
|
|
514
|
+
timeoutMs: DELEGATED_WAIT_TIMEOUT_MS,
|
|
515
|
+
})) as { status?: string; error?: string };
|
|
516
|
+
const status = typeof wait?.status === "string" ? wait.status : "error";
|
|
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
|
+
});
|
|
528
|
+
return jsonResult({
|
|
529
|
+
error: "lcm_expand_query timed out waiting for delegated expansion (120s).",
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
if (status !== "ok") {
|
|
533
|
+
return jsonResult({
|
|
534
|
+
error:
|
|
535
|
+
typeof wait?.error === "string" && wait.error.trim()
|
|
536
|
+
? wait.error
|
|
537
|
+
: "Delegated expansion query failed.",
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const replyPayload = (await input.deps.callGateway({
|
|
542
|
+
method: "sessions.get",
|
|
543
|
+
params: { key: childSessionKey, limit: 80 },
|
|
544
|
+
timeoutMs: GATEWAY_TIMEOUT_MS,
|
|
545
|
+
})) as { messages?: unknown[] };
|
|
546
|
+
const reply = input.deps.readLatestAssistantReply(
|
|
547
|
+
Array.isArray(replyPayload.messages) ? replyPayload.messages : [],
|
|
548
|
+
);
|
|
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
|
+
});
|
|
560
|
+
|
|
561
|
+
return jsonResult({
|
|
562
|
+
answer: parsed.answer,
|
|
563
|
+
citedIds: parsed.citedIds,
|
|
564
|
+
sourceConversationId,
|
|
565
|
+
expandedSummaryCount: parsed.expandedSummaryCount,
|
|
566
|
+
totalSourceTokens: parsed.totalSourceTokens,
|
|
567
|
+
truncated: parsed.truncated,
|
|
568
|
+
});
|
|
569
|
+
} catch (error) {
|
|
570
|
+
return jsonResult({
|
|
571
|
+
error: error instanceof Error ? error.message : String(error),
|
|
572
|
+
});
|
|
573
|
+
} finally {
|
|
574
|
+
if (childSessionKey) {
|
|
575
|
+
try {
|
|
576
|
+
await input.deps.callGateway({
|
|
577
|
+
method: "sessions.delete",
|
|
578
|
+
params: { key: childSessionKey, deleteTranscript: true },
|
|
579
|
+
timeoutMs: GATEWAY_TIMEOUT_MS,
|
|
580
|
+
});
|
|
581
|
+
} catch {
|
|
582
|
+
// Cleanup is best-effort.
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (grantCreated && childSessionKey) {
|
|
586
|
+
revokeDelegatedExpansionGrantForSession(childSessionKey, { removeBinding: true });
|
|
587
|
+
}
|
|
588
|
+
if (childSessionKey) {
|
|
589
|
+
clearDelegatedExpansionContext(childSessionKey);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
}
|