@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,351 @@
|
|
|
1
|
+
import type { ExpansionOrchestrator, ExpansionRequest, ExpansionResult } from "./expansion.js";
|
|
2
|
+
|
|
3
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export type ExpansionGrant = {
|
|
6
|
+
/** Unique grant ID */
|
|
7
|
+
grantId: string;
|
|
8
|
+
/** Session ID that issued the grant */
|
|
9
|
+
issuerSessionId: string;
|
|
10
|
+
/** Conversation IDs the grantee is allowed to traverse */
|
|
11
|
+
allowedConversationIds: number[];
|
|
12
|
+
/** Specific summary IDs the grantee is allowed to expand (if empty, all within conversation are allowed) */
|
|
13
|
+
allowedSummaryIds: string[];
|
|
14
|
+
/** Maximum traversal depth */
|
|
15
|
+
maxDepth: number;
|
|
16
|
+
/** Maximum tokens the grantee can retrieve */
|
|
17
|
+
tokenCap: number;
|
|
18
|
+
/** When the grant expires */
|
|
19
|
+
expiresAt: Date;
|
|
20
|
+
/** Whether this grant has been revoked */
|
|
21
|
+
revoked: boolean;
|
|
22
|
+
/** Creation timestamp */
|
|
23
|
+
createdAt: Date;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type CreateGrantInput = {
|
|
27
|
+
issuerSessionId: string;
|
|
28
|
+
allowedConversationIds: number[];
|
|
29
|
+
allowedSummaryIds?: string[];
|
|
30
|
+
maxDepth?: number;
|
|
31
|
+
tokenCap?: number;
|
|
32
|
+
/** TTL in milliseconds (default: 5 minutes) */
|
|
33
|
+
ttlMs?: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type CreateDelegatedExpansionGrantInput = CreateGrantInput & {
|
|
37
|
+
delegatedSessionKey: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type ValidationResult = {
|
|
41
|
+
valid: boolean;
|
|
42
|
+
reason?: string;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export type AuthorizedExpansionOrchestrator = {
|
|
46
|
+
expand(grantId: string, request: ExpansionRequest): Promise<ExpansionResult>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ── Defaults ─────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
const DEFAULT_MAX_DEPTH = 3;
|
|
52
|
+
const DEFAULT_TOKEN_CAP = 4000;
|
|
53
|
+
const DEFAULT_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
54
|
+
|
|
55
|
+
// ── ExpansionAuthManager ─────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
export class ExpansionAuthManager {
|
|
58
|
+
private grants: Map<string, ExpansionGrant> = new Map();
|
|
59
|
+
private consumedTokensByGrantId: Map<string, number> = new Map();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a new expansion grant with the given parameters.
|
|
63
|
+
* Generates a unique grant ID and applies defaults for optional fields.
|
|
64
|
+
*/
|
|
65
|
+
createGrant(input: CreateGrantInput): ExpansionGrant {
|
|
66
|
+
const grantId = "grant_" + crypto.randomUUID().slice(0, 12);
|
|
67
|
+
const now = new Date();
|
|
68
|
+
const ttlMs = input.ttlMs ?? DEFAULT_TTL_MS;
|
|
69
|
+
|
|
70
|
+
const grant: ExpansionGrant = {
|
|
71
|
+
grantId,
|
|
72
|
+
issuerSessionId: input.issuerSessionId,
|
|
73
|
+
allowedConversationIds: input.allowedConversationIds,
|
|
74
|
+
allowedSummaryIds: input.allowedSummaryIds ?? [],
|
|
75
|
+
maxDepth: input.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
76
|
+
tokenCap: input.tokenCap ?? DEFAULT_TOKEN_CAP,
|
|
77
|
+
expiresAt: new Date(now.getTime() + ttlMs),
|
|
78
|
+
revoked: false,
|
|
79
|
+
createdAt: now,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
this.grants.set(grantId, grant);
|
|
83
|
+
this.consumedTokensByGrantId.set(grantId, 0);
|
|
84
|
+
return grant;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Retrieve a grant by ID. Returns null if the grant does not exist,
|
|
89
|
+
* has been revoked, or has expired.
|
|
90
|
+
*/
|
|
91
|
+
getGrant(grantId: string): ExpansionGrant | null {
|
|
92
|
+
const grant = this.grants.get(grantId);
|
|
93
|
+
if (!grant) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (grant.revoked) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
if (grant.expiresAt.getTime() <= Date.now()) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return grant;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Revoke a grant, preventing any further use.
|
|
107
|
+
* Returns true if the grant was found and revoked, false if not found.
|
|
108
|
+
*/
|
|
109
|
+
revokeGrant(grantId: string): boolean {
|
|
110
|
+
const grant = this.grants.get(grantId);
|
|
111
|
+
if (!grant) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
grant.revoked = true;
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Resolve remaining token budget for an active grant.
|
|
120
|
+
*/
|
|
121
|
+
getRemainingTokenBudget(grantId: string): number | null {
|
|
122
|
+
const grant = this.getGrant(grantId);
|
|
123
|
+
if (!grant) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const consumed = Math.max(0, this.consumedTokensByGrantId.get(grantId) ?? 0);
|
|
127
|
+
return Math.max(0, Math.floor(grant.tokenCap) - consumed);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Consume token budget for a grant, clamped to the grant token cap.
|
|
132
|
+
*/
|
|
133
|
+
consumeTokenBudget(grantId: string, consumedTokens: number): number | null {
|
|
134
|
+
const grant = this.getGrant(grantId);
|
|
135
|
+
if (!grant) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const safeConsumed =
|
|
139
|
+
typeof consumedTokens === "number" && Number.isFinite(consumedTokens)
|
|
140
|
+
? Math.max(0, Math.floor(consumedTokens))
|
|
141
|
+
: 0;
|
|
142
|
+
const previous = Math.max(0, this.consumedTokensByGrantId.get(grantId) ?? 0);
|
|
143
|
+
const next = Math.min(Math.max(1, Math.floor(grant.tokenCap)), previous + safeConsumed);
|
|
144
|
+
this.consumedTokensByGrantId.set(grantId, next);
|
|
145
|
+
return Math.max(0, Math.floor(grant.tokenCap) - next);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Validate an expansion request against a grant.
|
|
150
|
+
* Checks existence, expiry, revocation, conversation scope, and summary scope.
|
|
151
|
+
*/
|
|
152
|
+
validateExpansion(
|
|
153
|
+
grantId: string,
|
|
154
|
+
request: {
|
|
155
|
+
conversationId: number;
|
|
156
|
+
summaryIds: string[];
|
|
157
|
+
depth: number;
|
|
158
|
+
tokenCap: number;
|
|
159
|
+
},
|
|
160
|
+
): ValidationResult {
|
|
161
|
+
const grant = this.grants.get(grantId);
|
|
162
|
+
|
|
163
|
+
// 1. Grant must exist
|
|
164
|
+
if (!grant) {
|
|
165
|
+
return { valid: false, reason: "Grant not found" };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 2. Grant must not be revoked
|
|
169
|
+
if (grant.revoked) {
|
|
170
|
+
return { valid: false, reason: "Grant has been revoked" };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 3. Grant must not be expired
|
|
174
|
+
if (grant.expiresAt.getTime() <= Date.now()) {
|
|
175
|
+
return { valid: false, reason: "Grant has expired" };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 4. Conversation ID must be in the allowed set
|
|
179
|
+
if (!grant.allowedConversationIds.includes(request.conversationId)) {
|
|
180
|
+
return {
|
|
181
|
+
valid: false,
|
|
182
|
+
reason: `Conversation ${request.conversationId} is not in the allowed set`,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 5. If allowedSummaryIds is non-empty, all requested summaryIds must be allowed
|
|
187
|
+
if (grant.allowedSummaryIds.length > 0) {
|
|
188
|
+
const allowedSet = new Set(grant.allowedSummaryIds);
|
|
189
|
+
const unauthorized = request.summaryIds.filter((id) => !allowedSet.has(id));
|
|
190
|
+
if (unauthorized.length > 0) {
|
|
191
|
+
return {
|
|
192
|
+
valid: false,
|
|
193
|
+
reason: `Summary IDs not authorized: ${unauthorized.join(", ")}`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { valid: true };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Remove all expired and revoked grants from the store.
|
|
203
|
+
* Returns the number of grants removed.
|
|
204
|
+
*/
|
|
205
|
+
cleanup(): number {
|
|
206
|
+
const now = Date.now();
|
|
207
|
+
let removed = 0;
|
|
208
|
+
|
|
209
|
+
for (const [grantId, grant] of this.grants) {
|
|
210
|
+
if (grant.revoked || grant.expiresAt.getTime() <= now) {
|
|
211
|
+
this.grants.delete(grantId);
|
|
212
|
+
this.consumedTokensByGrantId.delete(grantId);
|
|
213
|
+
removed++;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return removed;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const runtimeExpansionAuthManager = new ExpansionAuthManager();
|
|
222
|
+
const delegatedSessionGrantIds = new Map<string, string>();
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Return the singleton auth manager used by runtime delegated expansion flows.
|
|
226
|
+
*/
|
|
227
|
+
export function getRuntimeExpansionAuthManager(): ExpansionAuthManager {
|
|
228
|
+
return runtimeExpansionAuthManager;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Create a delegated expansion grant and bind it to the child session key.
|
|
233
|
+
*/
|
|
234
|
+
export function createDelegatedExpansionGrant(
|
|
235
|
+
input: CreateDelegatedExpansionGrantInput,
|
|
236
|
+
): ExpansionGrant {
|
|
237
|
+
const delegatedSessionKey = input.delegatedSessionKey.trim();
|
|
238
|
+
if (!delegatedSessionKey) {
|
|
239
|
+
throw new Error("delegatedSessionKey is required for delegated expansion grants");
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const grant = runtimeExpansionAuthManager.createGrant({
|
|
243
|
+
issuerSessionId: input.issuerSessionId,
|
|
244
|
+
allowedConversationIds: input.allowedConversationIds,
|
|
245
|
+
allowedSummaryIds: input.allowedSummaryIds,
|
|
246
|
+
maxDepth: input.maxDepth,
|
|
247
|
+
tokenCap: input.tokenCap,
|
|
248
|
+
ttlMs: input.ttlMs,
|
|
249
|
+
});
|
|
250
|
+
delegatedSessionGrantIds.set(delegatedSessionKey, grant.grantId);
|
|
251
|
+
return grant;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Resolve the delegated expansion grant id bound to a session key.
|
|
256
|
+
*/
|
|
257
|
+
export function resolveDelegatedExpansionGrantId(sessionKey: string): string | null {
|
|
258
|
+
const key = sessionKey.trim();
|
|
259
|
+
if (!key) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
return delegatedSessionGrantIds.get(key) ?? null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Revoke the delegated grant bound to a session key.
|
|
267
|
+
* Optionally remove the binding after revocation.
|
|
268
|
+
*/
|
|
269
|
+
export function revokeDelegatedExpansionGrantForSession(
|
|
270
|
+
sessionKey: string,
|
|
271
|
+
opts?: { removeBinding?: boolean },
|
|
272
|
+
): boolean {
|
|
273
|
+
const key = sessionKey.trim();
|
|
274
|
+
if (!key) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
const grantId = delegatedSessionGrantIds.get(key);
|
|
278
|
+
if (!grantId) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
const didRevoke = runtimeExpansionAuthManager.revokeGrant(grantId);
|
|
282
|
+
if (opts?.removeBinding) {
|
|
283
|
+
delegatedSessionGrantIds.delete(key);
|
|
284
|
+
}
|
|
285
|
+
return didRevoke;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Remove delegated grant binding for a session key without revoking.
|
|
290
|
+
*/
|
|
291
|
+
export function removeDelegatedExpansionGrantForSession(sessionKey: string): boolean {
|
|
292
|
+
const key = sessionKey.trim();
|
|
293
|
+
if (!key) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
return delegatedSessionGrantIds.delete(key);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Test-only reset helper for delegated runtime grants.
|
|
301
|
+
*/
|
|
302
|
+
export function resetDelegatedExpansionGrantsForTests(): void {
|
|
303
|
+
delegatedSessionGrantIds.clear();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── Authorized wrapper ───────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Create a thin authorization wrapper around an ExpansionOrchestrator.
|
|
310
|
+
* The wrapper validates the grant before delegating to the underlying
|
|
311
|
+
* orchestrator.
|
|
312
|
+
*/
|
|
313
|
+
export function wrapWithAuth(
|
|
314
|
+
orchestrator: ExpansionOrchestrator,
|
|
315
|
+
authManager: ExpansionAuthManager,
|
|
316
|
+
): AuthorizedExpansionOrchestrator {
|
|
317
|
+
return {
|
|
318
|
+
async expand(grantId: string, request: ExpansionRequest): Promise<ExpansionResult> {
|
|
319
|
+
const validation = authManager.validateExpansion(grantId, {
|
|
320
|
+
conversationId: request.conversationId,
|
|
321
|
+
summaryIds: request.summaryIds,
|
|
322
|
+
depth: request.maxDepth ?? DEFAULT_MAX_DEPTH,
|
|
323
|
+
tokenCap: request.tokenCap ?? DEFAULT_TOKEN_CAP,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
if (!validation.valid) {
|
|
327
|
+
throw new Error(`Expansion authorization failed: ${validation.reason}`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const remainingBudget = authManager.getRemainingTokenBudget(grantId);
|
|
331
|
+
if (remainingBudget == null) {
|
|
332
|
+
throw new Error("Expansion authorization failed: Grant not found");
|
|
333
|
+
}
|
|
334
|
+
if (remainingBudget <= 0) {
|
|
335
|
+
throw new Error("Expansion authorization failed: Grant token budget exhausted");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const requestedTokenCap =
|
|
339
|
+
typeof request.tokenCap === "number" && Number.isFinite(request.tokenCap)
|
|
340
|
+
? Math.max(1, Math.trunc(request.tokenCap))
|
|
341
|
+
: remainingBudget;
|
|
342
|
+
const effectiveTokenCap = Math.max(1, Math.min(requestedTokenCap, remainingBudget));
|
|
343
|
+
const result = await orchestrator.expand({
|
|
344
|
+
...request,
|
|
345
|
+
tokenCap: effectiveTokenCap,
|
|
346
|
+
});
|
|
347
|
+
authManager.consumeTokenBudget(grantId, result.totalTokens);
|
|
348
|
+
return result;
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
export type LcmExpansionRoutingIntent = "query_probe" | "explicit_expand";
|
|
2
|
+
|
|
3
|
+
export type LcmExpansionRoutingAction = "answer_directly" | "expand_shallow" | "delegate_traversal";
|
|
4
|
+
|
|
5
|
+
export type LcmExpansionTokenRiskLevel = "low" | "moderate" | "high";
|
|
6
|
+
|
|
7
|
+
export type LcmExpansionRoutingInput = {
|
|
8
|
+
intent: LcmExpansionRoutingIntent;
|
|
9
|
+
query?: string;
|
|
10
|
+
requestedMaxDepth?: number;
|
|
11
|
+
candidateSummaryCount: number;
|
|
12
|
+
tokenCap: number;
|
|
13
|
+
includeMessages?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type LcmExpansionRoutingDecision = {
|
|
17
|
+
action: LcmExpansionRoutingAction;
|
|
18
|
+
normalizedMaxDepth: number;
|
|
19
|
+
candidateSummaryCount: number;
|
|
20
|
+
estimatedTokens: number;
|
|
21
|
+
tokenCap: number;
|
|
22
|
+
tokenRiskRatio: number;
|
|
23
|
+
tokenRiskLevel: LcmExpansionTokenRiskLevel;
|
|
24
|
+
indicators: {
|
|
25
|
+
broadTimeRange: boolean;
|
|
26
|
+
multiHopRetrieval: boolean;
|
|
27
|
+
};
|
|
28
|
+
triggers: {
|
|
29
|
+
directByNoCandidates: boolean;
|
|
30
|
+
directByLowComplexityProbe: boolean;
|
|
31
|
+
delegateByDepth: boolean;
|
|
32
|
+
delegateByCandidateCount: boolean;
|
|
33
|
+
delegateByTokenRisk: boolean;
|
|
34
|
+
delegateByBroadTimeRangeAndMultiHop: boolean;
|
|
35
|
+
};
|
|
36
|
+
reasons: string[];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const EXPANSION_ROUTING_THRESHOLDS = {
|
|
40
|
+
defaultDepth: 3,
|
|
41
|
+
minDepth: 1,
|
|
42
|
+
maxDepth: 10,
|
|
43
|
+
directMaxDepth: 2,
|
|
44
|
+
directMaxCandidates: 1,
|
|
45
|
+
moderateTokenRiskRatio: 0.35,
|
|
46
|
+
highTokenRiskRatio: 0.7,
|
|
47
|
+
baseTokensPerSummary: 220,
|
|
48
|
+
includeMessagesTokenMultiplier: 1.9,
|
|
49
|
+
perDepthTokenGrowth: 0.65,
|
|
50
|
+
broadTimeRangeTokenMultiplier: 1.35,
|
|
51
|
+
multiHopTokenMultiplier: 1.25,
|
|
52
|
+
multiHopDepthThreshold: 3,
|
|
53
|
+
multiHopCandidateThreshold: 5,
|
|
54
|
+
} as const;
|
|
55
|
+
|
|
56
|
+
const BROAD_TIME_RANGE_PATTERNS = [
|
|
57
|
+
/\b(last|past)\s+(month|months|quarter|quarters|year|years)\b/i,
|
|
58
|
+
/\b(over|across|throughout)\s+(time|months|quarters|years)\b/i,
|
|
59
|
+
/\b(timeline|chronology|history|long[-\s]?term)\b/i,
|
|
60
|
+
/\bbetween\s+[^.]{0,40}\s+and\s+[^.]{0,40}\b/i,
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const MULTI_HOP_QUERY_PATTERNS = [
|
|
64
|
+
/\b(root\s+cause|causal\s+chain|chain\s+of\s+events)\b/i,
|
|
65
|
+
/\b(multi[-\s]?hop|multi[-\s]?step|cross[-\s]?summary)\b/i,
|
|
66
|
+
/\bhow\s+did\b.+\blead\s+to\b/i,
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
/** Normalize a requested depth to a deterministic bounded value. */
|
|
70
|
+
function normalizeDepth(requestedMaxDepth?: number): number {
|
|
71
|
+
if (typeof requestedMaxDepth !== "number" || !Number.isFinite(requestedMaxDepth)) {
|
|
72
|
+
return EXPANSION_ROUTING_THRESHOLDS.defaultDepth;
|
|
73
|
+
}
|
|
74
|
+
const rounded = Math.trunc(requestedMaxDepth);
|
|
75
|
+
return Math.max(
|
|
76
|
+
EXPANSION_ROUTING_THRESHOLDS.minDepth,
|
|
77
|
+
Math.min(EXPANSION_ROUTING_THRESHOLDS.maxDepth, rounded),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Normalize token cap to a positive bounded value for risk computation. */
|
|
82
|
+
function normalizeTokenCap(tokenCap: number): number {
|
|
83
|
+
if (!Number.isFinite(tokenCap)) {
|
|
84
|
+
return Number.MAX_SAFE_INTEGER;
|
|
85
|
+
}
|
|
86
|
+
return Math.max(1, Math.trunc(tokenCap));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Detect broad time-range intent from the user query.
|
|
91
|
+
*
|
|
92
|
+
* This is a deterministic text heuristic used by orchestration policy only;
|
|
93
|
+
* it does not perform retrieval.
|
|
94
|
+
*/
|
|
95
|
+
export function detectBroadTimeRangeIndicator(query?: string): boolean {
|
|
96
|
+
if (!query) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const trimmed = query.trim();
|
|
100
|
+
if (!trimmed) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (BROAD_TIME_RANGE_PATTERNS.some((pattern) => pattern.test(trimmed))) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const years = Array.from(trimmed.matchAll(/\b(?:19|20)\d{2}\b/g), (match) => Number(match[0]));
|
|
109
|
+
if (years.length < 2) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const earliest = Math.min(...years);
|
|
114
|
+
const latest = Math.max(...years);
|
|
115
|
+
return latest - earliest >= 2;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Detect whether traversal likely requires multi-hop expansion.
|
|
120
|
+
*
|
|
121
|
+
* Multi-hop is inferred from depth, breadth, and explicit language in the query.
|
|
122
|
+
*/
|
|
123
|
+
export function detectMultiHopIndicator(input: {
|
|
124
|
+
query?: string;
|
|
125
|
+
requestedMaxDepth?: number;
|
|
126
|
+
candidateSummaryCount: number;
|
|
127
|
+
}): boolean {
|
|
128
|
+
const normalizedMaxDepth = normalizeDepth(input.requestedMaxDepth);
|
|
129
|
+
const candidateSummaryCount = Math.max(0, Math.trunc(input.candidateSummaryCount));
|
|
130
|
+
|
|
131
|
+
if (normalizedMaxDepth >= EXPANSION_ROUTING_THRESHOLDS.multiHopDepthThreshold) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
if (candidateSummaryCount >= EXPANSION_ROUTING_THRESHOLDS.multiHopCandidateThreshold) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
if (!input.query) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const trimmed = input.query.trim();
|
|
142
|
+
if (!trimmed) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return MULTI_HOP_QUERY_PATTERNS.some((pattern) => pattern.test(trimmed));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Estimate expansion token volume from traversal characteristics.
|
|
150
|
+
*
|
|
151
|
+
* This deterministic estimate intentionally over-approximates near deep/broad
|
|
152
|
+
* traversals so delegation triggers before hitting hard runtime caps.
|
|
153
|
+
*/
|
|
154
|
+
export function estimateExpansionTokens(input: {
|
|
155
|
+
requestedMaxDepth?: number;
|
|
156
|
+
candidateSummaryCount: number;
|
|
157
|
+
includeMessages?: boolean;
|
|
158
|
+
broadTimeRangeIndicator?: boolean;
|
|
159
|
+
multiHopIndicator?: boolean;
|
|
160
|
+
}): number {
|
|
161
|
+
const normalizedMaxDepth = normalizeDepth(input.requestedMaxDepth);
|
|
162
|
+
const candidateSummaryCount = Math.max(0, Math.trunc(input.candidateSummaryCount));
|
|
163
|
+
if (candidateSummaryCount === 0) {
|
|
164
|
+
return 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const includeMessagesMultiplier = input.includeMessages
|
|
168
|
+
? EXPANSION_ROUTING_THRESHOLDS.includeMessagesTokenMultiplier
|
|
169
|
+
: 1;
|
|
170
|
+
const depthMultiplier =
|
|
171
|
+
1 + (normalizedMaxDepth - 1) * EXPANSION_ROUTING_THRESHOLDS.perDepthTokenGrowth;
|
|
172
|
+
const timeRangeMultiplier = input.broadTimeRangeIndicator
|
|
173
|
+
? EXPANSION_ROUTING_THRESHOLDS.broadTimeRangeTokenMultiplier
|
|
174
|
+
: 1;
|
|
175
|
+
const multiHopMultiplier = input.multiHopIndicator
|
|
176
|
+
? EXPANSION_ROUTING_THRESHOLDS.multiHopTokenMultiplier
|
|
177
|
+
: 1;
|
|
178
|
+
|
|
179
|
+
const perSummaryEstimate =
|
|
180
|
+
EXPANSION_ROUTING_THRESHOLDS.baseTokensPerSummary *
|
|
181
|
+
includeMessagesMultiplier *
|
|
182
|
+
depthMultiplier *
|
|
183
|
+
timeRangeMultiplier *
|
|
184
|
+
multiHopMultiplier;
|
|
185
|
+
|
|
186
|
+
return Math.max(0, Math.ceil(perSummaryEstimate * candidateSummaryCount));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Classify token risk level relative to a provided cap. */
|
|
190
|
+
export function classifyExpansionTokenRisk(input: { estimatedTokens: number; tokenCap: number }): {
|
|
191
|
+
ratio: number;
|
|
192
|
+
level: LcmExpansionTokenRiskLevel;
|
|
193
|
+
} {
|
|
194
|
+
const estimatedTokens = Math.max(0, Math.trunc(input.estimatedTokens));
|
|
195
|
+
const tokenCap = normalizeTokenCap(input.tokenCap);
|
|
196
|
+
const ratio = estimatedTokens / tokenCap;
|
|
197
|
+
|
|
198
|
+
if (ratio >= EXPANSION_ROUTING_THRESHOLDS.highTokenRiskRatio) {
|
|
199
|
+
return { ratio, level: "high" };
|
|
200
|
+
}
|
|
201
|
+
if (ratio >= EXPANSION_ROUTING_THRESHOLDS.moderateTokenRiskRatio) {
|
|
202
|
+
return { ratio, level: "moderate" };
|
|
203
|
+
}
|
|
204
|
+
return { ratio, level: "low" };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Decide deterministic route-vs-delegate policy for LCM expansion orchestration.
|
|
209
|
+
*
|
|
210
|
+
* The decision matrix supports three outcomes:
|
|
211
|
+
* - answer directly (skip expansion)
|
|
212
|
+
* - do shallow/direct expansion
|
|
213
|
+
* - delegate deep traversal to a sub-agent
|
|
214
|
+
*/
|
|
215
|
+
export function decideLcmExpansionRouting(
|
|
216
|
+
input: LcmExpansionRoutingInput,
|
|
217
|
+
): LcmExpansionRoutingDecision {
|
|
218
|
+
const normalizedMaxDepth = normalizeDepth(input.requestedMaxDepth);
|
|
219
|
+
const candidateSummaryCount = Math.max(0, Math.trunc(input.candidateSummaryCount));
|
|
220
|
+
const tokenCap = normalizeTokenCap(input.tokenCap);
|
|
221
|
+
const broadTimeRange = detectBroadTimeRangeIndicator(input.query);
|
|
222
|
+
const multiHopRetrieval = detectMultiHopIndicator({
|
|
223
|
+
query: input.query,
|
|
224
|
+
requestedMaxDepth: normalizedMaxDepth,
|
|
225
|
+
candidateSummaryCount,
|
|
226
|
+
});
|
|
227
|
+
const estimatedTokens = estimateExpansionTokens({
|
|
228
|
+
requestedMaxDepth: normalizedMaxDepth,
|
|
229
|
+
candidateSummaryCount,
|
|
230
|
+
includeMessages: input.includeMessages,
|
|
231
|
+
broadTimeRangeIndicator: broadTimeRange,
|
|
232
|
+
multiHopIndicator: multiHopRetrieval,
|
|
233
|
+
});
|
|
234
|
+
const tokenRisk = classifyExpansionTokenRisk({ estimatedTokens, tokenCap });
|
|
235
|
+
|
|
236
|
+
const directByNoCandidates = candidateSummaryCount === 0;
|
|
237
|
+
const directByLowComplexityProbe =
|
|
238
|
+
input.intent === "query_probe" &&
|
|
239
|
+
!directByNoCandidates &&
|
|
240
|
+
normalizedMaxDepth <= EXPANSION_ROUTING_THRESHOLDS.directMaxDepth &&
|
|
241
|
+
candidateSummaryCount <= EXPANSION_ROUTING_THRESHOLDS.directMaxCandidates &&
|
|
242
|
+
tokenRisk.level === "low" &&
|
|
243
|
+
!broadTimeRange &&
|
|
244
|
+
!multiHopRetrieval;
|
|
245
|
+
|
|
246
|
+
const delegateByDepth = false;
|
|
247
|
+
const delegateByCandidateCount = false;
|
|
248
|
+
const delegateByTokenRisk = tokenRisk.level === "high";
|
|
249
|
+
const delegateByBroadTimeRangeAndMultiHop = broadTimeRange && multiHopRetrieval;
|
|
250
|
+
|
|
251
|
+
const shouldDirect = directByNoCandidates || directByLowComplexityProbe;
|
|
252
|
+
const shouldDelegate =
|
|
253
|
+
!shouldDirect && (delegateByTokenRisk || delegateByBroadTimeRangeAndMultiHop);
|
|
254
|
+
|
|
255
|
+
const action: LcmExpansionRoutingAction = shouldDirect
|
|
256
|
+
? "answer_directly"
|
|
257
|
+
: shouldDelegate
|
|
258
|
+
? "delegate_traversal"
|
|
259
|
+
: "expand_shallow";
|
|
260
|
+
|
|
261
|
+
const reasons: string[] = [];
|
|
262
|
+
if (directByNoCandidates) {
|
|
263
|
+
reasons.push("No candidate summary IDs are available.");
|
|
264
|
+
}
|
|
265
|
+
if (directByLowComplexityProbe) {
|
|
266
|
+
reasons.push("Query probe is low complexity and below retrieval-risk thresholds.");
|
|
267
|
+
}
|
|
268
|
+
if (delegateByTokenRisk) {
|
|
269
|
+
reasons.push(
|
|
270
|
+
`Estimated token risk ratio ${tokenRisk.ratio.toFixed(2)} meets delegate threshold ` +
|
|
271
|
+
`${EXPANSION_ROUTING_THRESHOLDS.highTokenRiskRatio.toFixed(2)}.`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
if (delegateByBroadTimeRangeAndMultiHop) {
|
|
275
|
+
reasons.push("Broad time-range request combined with multi-hop retrieval indicators.");
|
|
276
|
+
}
|
|
277
|
+
if (action === "expand_shallow") {
|
|
278
|
+
reasons.push("Complexity is bounded; use direct/shallow expansion.");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
action,
|
|
283
|
+
normalizedMaxDepth,
|
|
284
|
+
candidateSummaryCount,
|
|
285
|
+
estimatedTokens,
|
|
286
|
+
tokenCap,
|
|
287
|
+
tokenRiskRatio: tokenRisk.ratio,
|
|
288
|
+
tokenRiskLevel: tokenRisk.level,
|
|
289
|
+
indicators: {
|
|
290
|
+
broadTimeRange,
|
|
291
|
+
multiHopRetrieval,
|
|
292
|
+
},
|
|
293
|
+
triggers: {
|
|
294
|
+
directByNoCandidates,
|
|
295
|
+
directByLowComplexityProbe,
|
|
296
|
+
delegateByDepth,
|
|
297
|
+
delegateByCandidateCount,
|
|
298
|
+
delegateByTokenRisk,
|
|
299
|
+
delegateByBroadTimeRangeAndMultiHop,
|
|
300
|
+
},
|
|
301
|
+
reasons,
|
|
302
|
+
};
|
|
303
|
+
}
|