@unson/brainbase-mcp 0.6.0 → 0.8.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.
Files changed (148) hide show
  1. package/contracts/foundation/graph-history.sql +634 -0
  2. package/contracts/judgment-value-proof/schema.json +43 -1
  3. package/dist/canonical-task-contract.d.ts +9 -0
  4. package/dist/canonical-task-contract.js +39 -0
  5. package/dist/canonical-task-principal.d.ts +16 -0
  6. package/dist/canonical-task-principal.js +36 -0
  7. package/dist/canonical-task-service.d.ts +251 -0
  8. package/dist/canonical-task-service.js +820 -0
  9. package/dist/cli.d.ts +2 -0
  10. package/dist/cli.js +107 -0
  11. package/dist/company-os-evaluation.d.ts +193 -0
  12. package/dist/company-os-evaluation.js +1150 -0
  13. package/dist/company-os-impact-review.d.ts +248 -0
  14. package/dist/company-os-impact-review.js +663 -0
  15. package/dist/company-os-learning-adoption.d.ts +288 -0
  16. package/dist/company-os-learning-adoption.js +944 -0
  17. package/dist/company-os-objectives.d.ts +63 -0
  18. package/dist/company-os-objectives.js +234 -0
  19. package/dist/company-os-receipt-adapter.d.ts +109 -0
  20. package/dist/company-os-receipt-adapter.js +636 -0
  21. package/dist/constraint-exception-store.d.ts +23 -0
  22. package/dist/constraint-exception-store.js +108 -0
  23. package/dist/constraint-resolution.d.ts +175 -0
  24. package/dist/constraint-resolution.js +922 -0
  25. package/dist/decision-adapter.d.ts +145 -0
  26. package/dist/decision-adapter.js +586 -0
  27. package/dist/durable-wait-http.d.ts +51 -0
  28. package/dist/durable-wait-http.js +497 -0
  29. package/dist/durable-waits.d.ts +234 -0
  30. package/dist/durable-waits.js +944 -0
  31. package/dist/execution-authority-http.d.ts +28 -0
  32. package/dist/execution-authority-http.js +359 -0
  33. package/dist/execution-authority.d.ts +201 -0
  34. package/dist/execution-authority.js +634 -0
  35. package/dist/foundation-authenticated-tools.d.ts +56 -0
  36. package/dist/foundation-authenticated-tools.js +347 -0
  37. package/dist/foundation-catalog.d.ts +26 -0
  38. package/dist/foundation-catalog.js +206 -0
  39. package/dist/foundation-constraint-store.d.ts +50 -0
  40. package/dist/foundation-constraint-store.js +215 -0
  41. package/dist/foundation-graph-http.d.ts +43 -0
  42. package/dist/foundation-graph-http.js +243 -0
  43. package/dist/foundation-graph-write.d.ts +90 -0
  44. package/dist/foundation-graph-write.js +447 -0
  45. package/dist/foundation-http.d.ts +90 -0
  46. package/dist/foundation-http.js +439 -0
  47. package/dist/foundation-public-provider.d.ts +140 -0
  48. package/dist/foundation-public-provider.js +192 -0
  49. package/dist/foundation-store.d.ts +132 -0
  50. package/dist/foundation-store.js +654 -0
  51. package/dist/graph-corrections.d.ts +157 -0
  52. package/dist/graph-corrections.js +620 -0
  53. package/dist/graph-foundation-reader.d.ts +60 -0
  54. package/dist/graph-foundation-reader.js +494 -0
  55. package/dist/graph-retrieval.d.ts +6 -1
  56. package/dist/graph-revision-reader.d.ts +80 -0
  57. package/dist/graph-revision-reader.js +246 -0
  58. package/dist/graph-web-http.d.ts +26 -0
  59. package/dist/graph-web-http.js +215 -0
  60. package/dist/graph-web.d.ts +364 -0
  61. package/dist/graph-web.js +639 -0
  62. package/dist/judgment-dag-composition.d.ts +224 -0
  63. package/dist/judgment-dag-composition.js +731 -0
  64. package/dist/judgment-dag.d.ts +2 -0
  65. package/dist/judgment-dag.js +1 -0
  66. package/dist/judgment-problem-snapshot.d.ts +213 -0
  67. package/dist/judgment-problem-snapshot.js +1073 -0
  68. package/dist/judgment-value-proof-review.d.ts +166 -0
  69. package/dist/judgment-value-proof-review.js +437 -0
  70. package/dist/judgment-value-proof.d.ts +28 -1
  71. package/dist/judgment-value-proof.js +53 -8
  72. package/dist/judgment-view-http.d.ts +22 -0
  73. package/dist/judgment-view-http.js +128 -0
  74. package/dist/judgment-view.d.ts +222 -0
  75. package/dist/judgment-view.js +627 -0
  76. package/dist/knowledge-adapter.d.ts +112 -0
  77. package/dist/knowledge-adapter.js +691 -0
  78. package/dist/knowledge-continuation.d.ts +127 -0
  79. package/dist/knowledge-continuation.js +446 -0
  80. package/dist/knowledge-lookup.d.ts +178 -0
  81. package/dist/knowledge-lookup.js +758 -0
  82. package/dist/local-web-fetch-bridge.d.ts +5 -0
  83. package/dist/local-web-fetch-bridge.js +37 -0
  84. package/dist/local-web-host.d.ts +109 -0
  85. package/dist/local-web-host.js +688 -0
  86. package/dist/local-web-security.d.ts +25 -0
  87. package/dist/local-web-security.js +95 -0
  88. package/dist/ontology-foundation.d.ts +209 -0
  89. package/dist/ontology-foundation.js +1000 -0
  90. package/dist/ontology.d.ts +13 -0
  91. package/dist/ontology.js +14 -0
  92. package/dist/organization-connection.d.ts +164 -0
  93. package/dist/organization-connection.js +428 -0
  94. package/dist/organization-graph.d.ts +15 -0
  95. package/dist/organization-graph.js +24 -4
  96. package/dist/personal-knowledge.d.ts +132 -0
  97. package/dist/personal-knowledge.js +779 -0
  98. package/dist/personal-memory-handover.d.ts +108 -0
  99. package/dist/personal-memory-handover.js +369 -0
  100. package/dist/philosophy-revision-reader.d.ts +85 -0
  101. package/dist/philosophy-revision-reader.js +241 -0
  102. package/dist/portable-graph.d.ts +24 -0
  103. package/dist/portable-graph.js +77 -0
  104. package/dist/problem-candidates.d.ts +149 -0
  105. package/dist/problem-candidates.js +898 -0
  106. package/dist/problem-selection.d.ts +238 -0
  107. package/dist/problem-selection.js +995 -0
  108. package/dist/resource-reservation-http.d.ts +31 -0
  109. package/dist/resource-reservation-http.js +371 -0
  110. package/dist/resource-reservations.d.ts +221 -0
  111. package/dist/resource-reservations.js +577 -0
  112. package/dist/server.d.ts +104 -2
  113. package/dist/server.js +186 -4
  114. package/dist/ssot.d.ts +6 -1
  115. package/dist/ssot.js +63 -5
  116. package/dist/streamable-http.d.ts +42 -0
  117. package/dist/streamable-http.js +288 -0
  118. package/dist/types.d.ts +20 -0
  119. package/dist/value-proof-review-http.d.ts +25 -0
  120. package/dist/value-proof-review-http.js +185 -0
  121. package/dist/world-model.d.ts +199 -0
  122. package/dist/world-model.js +955 -0
  123. package/package.json +230 -3
  124. package/ui/brainbase-tokens.css +56 -0
  125. package/ui/graph-own-share.js +367 -0
  126. package/ui/graph-projects-view.css +23 -0
  127. package/ui/graph-projects-view.js +730 -0
  128. package/ui/graph-registry-view.css +101 -0
  129. package/ui/graph-registry-view.js +804 -0
  130. package/ui/graph-view-shared.css +522 -0
  131. package/ui/graph-view-shared.js +1227 -0
  132. package/ui/judgment-view.css +312 -0
  133. package/ui/judgment-view.js +508 -0
  134. package/ui/local-web-shell.css +396 -0
  135. package/ui/local-web-shell.js +378 -0
  136. package/ui/objective-editor-http-port.js +154 -0
  137. package/ui/objective-editor.css +286 -0
  138. package/ui/objective-editor.js +1586 -0
  139. package/ui/outcome-knowledge.css +50 -14
  140. package/ui/outcome-knowledge.js +15 -1
  141. package/ui/outcome-mana.css +91 -14
  142. package/ui/outcome-mana.js +958 -61
  143. package/ui/value-proof-review.css +504 -0
  144. package/ui/value-proof-review.js +1104 -0
  145. package/ui/workspace-kit.css +485 -0
  146. package/ui/workspace-kit.js +229 -0
  147. package/ui/world-model-view.css +188 -0
  148. package/ui/world-model-view.js +486 -0
@@ -0,0 +1,758 @@
1
+ const MAX_TEXT_LENGTH = 6_000;
2
+ const MAX_FIELD_LENGTH = 200;
3
+ const MAX_FIELDS = 40;
4
+ const MAX_CONTEXT_HINTS = 20;
5
+ const MAX_HINT_LENGTH = 1_000;
6
+ const MAX_IDS = 10;
7
+ const MAX_RELATION_LENGTH = 200;
8
+ /** Public roots accepted in a projection. Unknown payload roots are dropped. */
9
+ export const KNOWLEDGE_LOOKUP_ALLOWED_FIELD_ROOTS = new Set([
10
+ 'id',
11
+ 'graph_entity_id',
12
+ 'type',
13
+ 'project_code',
14
+ 'organization_id',
15
+ 'tenant_id',
16
+ 'updated',
17
+ 'updated_at',
18
+ 'version',
19
+ 'name',
20
+ 'title',
21
+ 'status',
22
+ 'description',
23
+ 'content',
24
+ 'body',
25
+ 'summary',
26
+ 'statement',
27
+ 'rationale',
28
+ 'decision',
29
+ 'markdown',
30
+ 'body_summary',
31
+ 'notes',
32
+ 'app_id',
33
+ 'display_name',
34
+ 'excerpt',
35
+ 'project',
36
+ 'orgs',
37
+ 'org',
38
+ 'org_tags',
39
+ 'aliases',
40
+ 'projects',
41
+ 'repository',
42
+ 'parent_project',
43
+ 'parent_project_entity_id',
44
+ 'environments',
45
+ 'environment',
46
+ 'record_class',
47
+ 'service_profile_eligible',
48
+ 'operational_profile_coverage',
49
+ 'source',
50
+ 'source_path',
51
+ 'legacy_source_path',
52
+ 'lifecycle_status',
53
+ 'lifecycle_state',
54
+ 'semantic_state',
55
+ 'related_orgs',
56
+ 'related_apps',
57
+ 'owner_entity_id',
58
+ 'tags',
59
+ 'path',
60
+ 'role',
61
+ 'orgType',
62
+ 'canonical',
63
+ 'term',
64
+ 'decided_at',
65
+ 'decider',
66
+ 'project_id',
67
+ 'meeting_id',
68
+ 'salesOrg',
69
+ 'implOrg',
70
+ 'contractType',
71
+ 'upfront',
72
+ 'customer_id',
73
+ 'partner',
74
+ ]);
75
+ const FORBIDDEN_FIELD_ROOTS = new Set(['payload', 'filePath', 'retrieval_evidence', '__proto__', 'constructor', 'prototype']);
76
+ const DEFAULT_ENTITY_TYPES = ['decision', 'project', 'org', 'app', 'philosophy', 'glossary_term', 'document', 'person', 'brand'];
77
+ const isRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
78
+ const nonEmptyString = (value, max = MAX_TEXT_LENGTH) => typeof value === 'string' && value.trim().length > 0 && value.length <= max;
79
+ function normalizeStringList(value, label, options) {
80
+ if (!Array.isArray(value) || value.length > options.maxItems
81
+ || !value.every((item) => nonEmptyString(item, options.maxLength))) {
82
+ throw new LookupInputError('brainbase_knowledge_lookup_input_invalid', `${label} must be a bounded list of non-empty strings`);
83
+ }
84
+ const values = value.map((item) => item.trim());
85
+ if (new Set(values).size !== values.length) {
86
+ throw new LookupInputError('brainbase_knowledge_lookup_input_invalid', `${label} must not contain duplicates`);
87
+ }
88
+ return values;
89
+ }
90
+ function normalizeFields(value, label = 'required_fields') {
91
+ const fields = normalizeStringList(value, label, { maxItems: MAX_FIELDS, maxLength: MAX_FIELD_LENGTH });
92
+ for (const field of fields) {
93
+ const root = field.split('.')[0];
94
+ if (FORBIDDEN_FIELD_ROOTS.has(root) || !KNOWLEDGE_LOOKUP_ALLOWED_FIELD_ROOTS.has(root)
95
+ || field.includes('..') || field.includes('[') || field.includes(']')) {
96
+ throw new LookupInputError('brainbase_knowledge_lookup_field_invalid', `Unsupported field path: ${field}`);
97
+ }
98
+ }
99
+ return fields;
100
+ }
101
+ export function validateKnowledgeLookupFields(value, label = 'required_fields') {
102
+ try {
103
+ return { valid: true, fields: normalizeFields(value, label) };
104
+ }
105
+ catch (error) {
106
+ if (error instanceof LookupInputError) {
107
+ return { valid: false, code: error.code, message: error.message };
108
+ }
109
+ throw error;
110
+ }
111
+ }
112
+ function normalizeIds(value, label) {
113
+ const ids = normalizeStringList(value, label, { maxItems: MAX_IDS, maxLength: 1_000 });
114
+ if (ids.some((id) => id.includes(','))) {
115
+ throw new LookupInputError('brainbase_knowledge_lookup_input_invalid', `${label} must contain single Graph ids`);
116
+ }
117
+ return ids;
118
+ }
119
+ class LookupInputError extends Error {
120
+ code;
121
+ constructor(code, message) {
122
+ super(message);
123
+ this.code = code;
124
+ }
125
+ }
126
+ /**
127
+ * Validate and normalize the public finish proposal shape. The continuation
128
+ * host calls this before its evidence checks so the API and host enforce the
129
+ * same bounds, identifier rules, and public field paths.
130
+ */
131
+ export function validateKnowledgeLookupFinish(value) {
132
+ try {
133
+ if (!isRecord(value)) {
134
+ throw new LookupInputError('brainbase_knowledge_lookup_finish_invalid', 'finish action must be an object');
135
+ }
136
+ if (value.assessment !== 'sufficient' && value.assessment !== 'insufficient') {
137
+ throw new LookupInputError('brainbase_knowledge_lookup_finish_invalid', 'finish assessment must be sufficient or insufficient');
138
+ }
139
+ if (value.status !== 'satisfied' && value.status !== 'unresolved' && value.status !== 'needs_user_input') {
140
+ throw new LookupInputError('brainbase_knowledge_lookup_finish_invalid', 'finish status is invalid');
141
+ }
142
+ const referenceIds = normalizeIds(value.reference_ids, 'reference_ids');
143
+ const unresolvedItems = normalizeStringList(value.unresolved_items, 'unresolved_items', {
144
+ maxItems: MAX_FIELDS,
145
+ maxLength: MAX_HINT_LENGTH,
146
+ });
147
+ if (!nonEmptyString(value.termination_reason, MAX_HINT_LENGTH)) {
148
+ throw new LookupInputError('brainbase_knowledge_lookup_finish_invalid', 'termination_reason must be non-empty');
149
+ }
150
+ if (!Array.isArray(value.field_evidence) || value.field_evidence.length > MAX_FIELDS) {
151
+ throw new LookupInputError('brainbase_knowledge_lookup_finish_invalid', 'field_evidence must be a bounded list');
152
+ }
153
+ const fieldEvidence = value.field_evidence.map((item, index) => {
154
+ if (!isRecord(item) || !nonEmptyString(item.field, MAX_FIELD_LENGTH)
155
+ || !nonEmptyString(item.reference_id, 1_000) || !nonEmptyString(item.attempt_id, 300)) {
156
+ throw new LookupInputError('brainbase_knowledge_lookup_finish_invalid', `field_evidence[${index}] is invalid`);
157
+ }
158
+ normalizeFields([item.field], `field_evidence[${index}].field`);
159
+ return {
160
+ field: item.field.trim(),
161
+ reference_id: item.reference_id.trim(),
162
+ attempt_id: item.attempt_id.trim(),
163
+ };
164
+ });
165
+ return {
166
+ valid: true,
167
+ finish: {
168
+ assessment: value.assessment,
169
+ status: value.status,
170
+ reference_ids: referenceIds,
171
+ field_evidence: fieldEvidence,
172
+ unresolved_items: unresolvedItems,
173
+ termination_reason: value.termination_reason.trim(),
174
+ },
175
+ };
176
+ }
177
+ catch (error) {
178
+ if (error instanceof LookupInputError) {
179
+ return { valid: false, code: error.code, message: error.message };
180
+ }
181
+ throw error;
182
+ }
183
+ }
184
+ function metadata(args) {
185
+ const lookupId = args.lookup_id;
186
+ const revision = args.revision;
187
+ const attemptId = args.attempt_id;
188
+ if (lookupId !== undefined && !nonEmptyString(lookupId, 200)) {
189
+ throw new LookupInputError('brainbase_knowledge_lookup_input_invalid', 'lookup_id must be a non-empty string');
190
+ }
191
+ if (revision !== undefined && (!Number.isInteger(revision) || Number(revision) < 0)) {
192
+ throw new LookupInputError('brainbase_knowledge_lookup_input_invalid', 'revision must be a non-negative integer');
193
+ }
194
+ if (attemptId !== undefined && !nonEmptyString(attemptId, 300)) {
195
+ throw new LookupInputError('brainbase_knowledge_lookup_input_invalid', 'attempt_id must be a non-empty string');
196
+ }
197
+ return {
198
+ lookup_id: typeof lookupId === 'string' ? lookupId : null,
199
+ revision: typeof revision === 'number' ? revision : null,
200
+ attempt_id: typeof attemptId === 'string' ? attemptId : null,
201
+ };
202
+ }
203
+ function commonInput(args) {
204
+ if (!nonEmptyString(args.question)) {
205
+ throw new LookupInputError('brainbase_knowledge_lookup_input_invalid', 'question must be a non-empty string');
206
+ }
207
+ if (!nonEmptyString(args.target_hint, MAX_HINT_LENGTH)) {
208
+ throw new LookupInputError('brainbase_knowledge_lookup_input_invalid', 'target_hint must be a non-empty string');
209
+ }
210
+ const requiredFields = normalizeFields(args.required_fields);
211
+ if (requiredFields.length === 0) {
212
+ throw new LookupInputError('brainbase_knowledge_lookup_input_invalid', 'required_fields must contain at least one field');
213
+ }
214
+ const knownEntityId = args.known_entity_id === undefined
215
+ ? undefined
216
+ : nonEmptyString(args.known_entity_id, 1_000) && !args.known_entity_id.includes(',')
217
+ ? args.known_entity_id.trim()
218
+ : (() => {
219
+ throw new LookupInputError('brainbase_knowledge_lookup_input_invalid', 'known_entity_id must be a single Graph id');
220
+ })();
221
+ const contextHints = args.context_hints === undefined
222
+ ? []
223
+ : normalizeStringList(args.context_hints, 'context_hints', { maxItems: MAX_CONTEXT_HINTS, maxLength: MAX_HINT_LENGTH });
224
+ const assessment = args.assessment === undefined
225
+ ? undefined
226
+ : args.assessment === 'sufficient' || args.assessment === 'insufficient'
227
+ ? args.assessment
228
+ : (() => {
229
+ throw new LookupInputError('brainbase_knowledge_lookup_input_invalid', 'assessment must be sufficient or insufficient');
230
+ })();
231
+ const missingInformation = args.missing_information === undefined
232
+ ? []
233
+ : normalizeStringList(args.missing_information, 'missing_information', { maxItems: MAX_FIELDS, maxLength: MAX_HINT_LENGTH });
234
+ const whyDifferent = args.why_different === undefined
235
+ ? undefined
236
+ : nonEmptyString(args.why_different, MAX_HINT_LENGTH)
237
+ ? args.why_different.trim()
238
+ : (() => {
239
+ throw new LookupInputError('brainbase_knowledge_lookup_input_invalid', 'why_different must be a non-empty string');
240
+ })();
241
+ const basedOnAttemptIds = args.based_on_attempt_ids === undefined
242
+ ? []
243
+ : normalizeStringList(args.based_on_attempt_ids, 'based_on_attempt_ids', { maxItems: MAX_IDS, maxLength: 300 });
244
+ return {
245
+ question: args.question.trim(),
246
+ targetHint: args.target_hint.trim(),
247
+ requiredFields,
248
+ ...(knownEntityId ? { knownEntityId } : {}),
249
+ contextHints,
250
+ metadata: metadata(args),
251
+ modelMetadata: {
252
+ ...(assessment ? { assessment } : {}),
253
+ missing_information: missingInformation,
254
+ ...(whyDifferent ? { why_different: whyDifferent } : {}),
255
+ based_on_attempt_ids: basedOnAttemptIds,
256
+ },
257
+ };
258
+ }
259
+ function queryForInput(input, actionQuery) {
260
+ const values = [
261
+ actionQuery,
262
+ input.question,
263
+ input.targetHint,
264
+ ...(input.knownEntityId ? [`known_entity_id: ${input.knownEntityId}`] : []),
265
+ ...input.contextHints,
266
+ ].filter((value) => typeof value === 'string' && value.trim().length > 0);
267
+ const query = values.join('\n').trim();
268
+ if (!query || query.length > MAX_TEXT_LENGTH) {
269
+ throw new LookupInputError('brainbase_knowledge_lookup_input_invalid', 'lookup query is empty or too long');
270
+ }
271
+ return query;
272
+ }
273
+ function validateEntityTypes(value) {
274
+ if (value === undefined)
275
+ return [...DEFAULT_ENTITY_TYPES];
276
+ return normalizeStringList(value, 'entity_types', { maxItems: 10, maxLength: 100 });
277
+ }
278
+ function normalizeAction(raw, input) {
279
+ if (raw === undefined)
280
+ return { kind: 'search', required_fields: input.requiredFields };
281
+ if (!isRecord(raw) || !nonEmptyString(raw.kind, 40)) {
282
+ throw new LookupInputError('brainbase_knowledge_lookup_action_invalid', 'next_action.kind is required');
283
+ }
284
+ const kind = raw.kind;
285
+ if (!['search', 'read', 'follow_relation', 'finish'].includes(kind)) {
286
+ throw new LookupInputError('brainbase_knowledge_lookup_action_unsupported', `Unsupported next action: ${String(raw.kind)}`);
287
+ }
288
+ const actionFields = raw.required_fields === undefined
289
+ ? input.requiredFields
290
+ : normalizeFields(raw.required_fields, 'next_action.required_fields');
291
+ const omittedRequiredFields = input.requiredFields.filter((field) => !actionFields.includes(field));
292
+ if (omittedRequiredFields.length > 0) {
293
+ throw new LookupInputError('brainbase_knowledge_lookup_action_invalid', `next_action.required_fields cannot omit top-level required_fields: ${omittedRequiredFields.join(', ')}`);
294
+ }
295
+ const whyDifferent = raw.why_different === undefined
296
+ ? undefined
297
+ : nonEmptyString(raw.why_different, MAX_HINT_LENGTH)
298
+ ? raw.why_different.trim()
299
+ : (() => {
300
+ throw new LookupInputError('brainbase_knowledge_lookup_action_invalid', 'why_different must be a non-empty string');
301
+ })();
302
+ if (kind === 'search') {
303
+ const query = raw.query === undefined
304
+ ? queryForInput(input)
305
+ : nonEmptyString(raw.query) ? raw.query.trim() : (() => {
306
+ throw new LookupInputError('brainbase_knowledge_lookup_action_invalid', 'search query must be non-empty');
307
+ })();
308
+ return {
309
+ kind,
310
+ required_fields: actionFields,
311
+ ...(whyDifferent ? { why_different: whyDifferent } : {}),
312
+ query,
313
+ entity_types: validateEntityTypes(raw.entity_types),
314
+ ...(raw.seed_ids === undefined ? {} : { seed_ids: normalizeIds(raw.seed_ids, 'next_action.seed_ids') }),
315
+ };
316
+ }
317
+ if (kind === 'read') {
318
+ if (!nonEmptyString(raw.entity_id, 1_000) || raw.entity_id.includes(',')) {
319
+ throw new LookupInputError('brainbase_knowledge_lookup_action_invalid', 'read entity_id must be a single Graph id');
320
+ }
321
+ if (!nonEmptyString(raw.entity_type, 100) || raw.entity_type.includes(',')) {
322
+ throw new LookupInputError('brainbase_knowledge_lookup_action_invalid', 'read entity_type must be a single type');
323
+ }
324
+ return {
325
+ kind,
326
+ required_fields: actionFields,
327
+ ...(whyDifferent ? { why_different: whyDifferent } : {}),
328
+ entity_id: raw.entity_id.trim(),
329
+ entity_type: raw.entity_type.trim(),
330
+ };
331
+ }
332
+ if (kind === 'follow_relation') {
333
+ const seedIds = normalizeIds(raw.seed_ids, 'next_action.seed_ids');
334
+ if (!nonEmptyString(raw.relation, MAX_RELATION_LENGTH)) {
335
+ throw new LookupInputError('brainbase_knowledge_lookup_action_invalid', 'relation must be a non-empty string');
336
+ }
337
+ if (raw.direction !== 'incoming' && raw.direction !== 'outgoing') {
338
+ throw new LookupInputError('brainbase_knowledge_lookup_action_invalid', 'direction must be incoming or outgoing');
339
+ }
340
+ const targetTypes = raw.target_types === undefined
341
+ ? undefined
342
+ : normalizeStringList(raw.target_types, 'target_types', { maxItems: 10, maxLength: 100 });
343
+ return {
344
+ kind,
345
+ required_fields: actionFields,
346
+ ...(whyDifferent ? { why_different: whyDifferent } : {}),
347
+ seed_ids: seedIds,
348
+ relation: raw.relation.trim(),
349
+ direction: raw.direction,
350
+ ...(targetTypes ? { target_types: targetTypes } : {}),
351
+ };
352
+ }
353
+ return normalizeFinish(raw, actionFields, whyDifferent);
354
+ }
355
+ function normalizeFinish(raw, requiredFields, whyDifferent) {
356
+ const validation = validateKnowledgeLookupFinish(raw);
357
+ if (!validation.valid)
358
+ throw new LookupInputError(validation.code, validation.message);
359
+ return {
360
+ kind: 'finish',
361
+ required_fields: requiredFields,
362
+ ...(whyDifferent ? { why_different: whyDifferent } : {}),
363
+ finish: validation.finish,
364
+ };
365
+ }
366
+ function flattenFields(value, prefix, output) {
367
+ if (value === undefined || value === null)
368
+ return;
369
+ if (typeof value !== 'object') {
370
+ if (String(value).trim())
371
+ output.add(prefix);
372
+ return;
373
+ }
374
+ if (Array.isArray(value)) {
375
+ if (value.length > 0)
376
+ output.add(prefix);
377
+ value.forEach((item, index) => flattenFields(item, `${prefix}.${index}`, output));
378
+ return;
379
+ }
380
+ const record = value;
381
+ if (Object.keys(record).length === 0)
382
+ return;
383
+ output.add(prefix);
384
+ for (const [key, child] of Object.entries(record))
385
+ flattenFields(child, `${prefix}.${key}`, output);
386
+ }
387
+ function publicProjection(raw, projection) {
388
+ const body = {};
389
+ for (const [key, value] of Object.entries(projection.body)) {
390
+ if (value === undefined || !KNOWLEDGE_LOOKUP_ALLOWED_FIELD_ROOTS.has(key)
391
+ || key === 'filePath' || key === 'retrieval_evidence')
392
+ continue;
393
+ body[key] = value;
394
+ }
395
+ const graphId = raw.id ?? raw.entity_id;
396
+ if (typeof graphId === 'string' && graphId.trim()) {
397
+ body.id = graphId;
398
+ body.graph_entity_id = graphId;
399
+ }
400
+ const rawType = raw.entity_type ?? raw.type;
401
+ const entityType = typeof projection.entity_type === 'string' && projection.entity_type.trim()
402
+ ? projection.entity_type
403
+ : typeof body.type === 'string' && body.type.trim()
404
+ ? body.type
405
+ : typeof rawType === 'string' && rawType.trim() ? rawType : 'unknown';
406
+ if (!body.type && entityType !== 'unknown')
407
+ body.type = entityType;
408
+ const fields = new Set();
409
+ for (const [key, value] of Object.entries(body))
410
+ flattenFields(value, key, fields);
411
+ return { body, entityType, fields: [...fields].sort() };
412
+ }
413
+ function requiredMissing(requiredFields, availableFields) {
414
+ const available = new Set(availableFields);
415
+ return requiredFields.filter((field) => !available.has(field));
416
+ }
417
+ function boundedCandidateValue(value) {
418
+ if (typeof value === 'string')
419
+ return value.length > 1_000 ? `${value.slice(0, 997)}...` : value;
420
+ if (typeof value === 'number' || typeof value === 'boolean' || value === null)
421
+ return value;
422
+ return undefined;
423
+ }
424
+ function candidateSummary(candidate, evidence) {
425
+ const id = candidate.id ?? candidate.entity_id;
426
+ const entityType = candidate.entity_type ?? candidate.type;
427
+ const summary = {
428
+ id: String(id),
429
+ entity_type: String(entityType),
430
+ };
431
+ for (const field of ['name', 'title', 'display_name', 'excerpt']) {
432
+ const value = boundedCandidateValue(candidate[field]);
433
+ if (typeof value === 'string' && value.trim())
434
+ summary[field] = value;
435
+ }
436
+ const publicEvidence = {};
437
+ for (const [field, value] of Object.entries(evidence)) {
438
+ const bounded = boundedCandidateValue(value);
439
+ if (bounded !== undefined)
440
+ publicEvidence[field] = bounded;
441
+ }
442
+ if (Object.keys(publicEvidence).length > 0)
443
+ summary.evidence = publicEvidence;
444
+ return summary;
445
+ }
446
+ function referencesFromCandidates(candidates) {
447
+ const references = [];
448
+ const available = new Set();
449
+ const summaries = [];
450
+ for (const candidate of candidates) {
451
+ if (!isRecord(candidate))
452
+ continue;
453
+ const id = candidate.id ?? candidate.entity_id;
454
+ const entityType = candidate.entity_type ?? candidate.type;
455
+ if (!nonEmptyString(id, 1_000) || !nonEmptyString(entityType, 100))
456
+ continue;
457
+ const evidence = isRecord(candidate.evidence) ? candidate.evidence : {};
458
+ const fields = Object.keys(evidence).filter((field) => evidence[field] !== undefined && evidence[field] !== null);
459
+ for (const field of fields)
460
+ available.add(field);
461
+ for (const field of ['name', 'title', 'display_name', 'excerpt']) {
462
+ if (typeof candidate[field] === 'string' && candidate[field].trim())
463
+ available.add(field);
464
+ }
465
+ references.push({ id: id.trim(), entity_type: entityType.trim(), evidence_fields: fields.sort() });
466
+ summaries.push(candidateSummary(candidate, evidence));
467
+ }
468
+ return { references, availableFields: [...available].sort(), summaries };
469
+ }
470
+ function baseResult(input, action, scope, data = {}) {
471
+ return {
472
+ lookup_id: input.metadata.lookup_id,
473
+ revision: input.metadata.revision,
474
+ attempt_id: input.metadata.attempt_id,
475
+ outcome: 'incomplete',
476
+ searched_scope: { project_codes: [...scope] },
477
+ references: [],
478
+ available_fields: [],
479
+ missing_fields: action.required_fields,
480
+ coverage: 'unknown',
481
+ absence_confirmed: false,
482
+ body: null,
483
+ action,
484
+ ...data,
485
+ model_metadata: input.modelMetadata,
486
+ };
487
+ }
488
+ function dependencyFailure(input, action, retrieval) {
489
+ const error = retrieval.error;
490
+ const httpStatus = error?.http_status;
491
+ const code = error?.code || 'brainbase_knowledge_lookup_failed';
492
+ const outcome = code === 'brainbase_project_not_accessible' || httpStatus === 401 || httpStatus === 403
493
+ ? 'forbidden'
494
+ : retrieval.status === 'unavailable' || (httpStatus !== undefined && httpStatus >= 500)
495
+ ? 'transport_error'
496
+ : 'unsupported';
497
+ const data = baseResult(input, action, retrieval.scope.project_codes, {
498
+ outcome,
499
+ coverage: outcome === 'transport_error' ? 'unknown' : 'complete',
500
+ error_kind: code,
501
+ });
502
+ return {
503
+ status: retrieval.status,
504
+ scope: retrieval.scope,
505
+ data,
506
+ ...(error ? { error } : {}),
507
+ };
508
+ }
509
+ function success(input, action, scope, data) {
510
+ return { status: 'ok', scope: { project_codes: scope }, data: baseResult(input, action, scope, data) };
511
+ }
512
+ function coverageOf(value) {
513
+ return value === 'complete' || value === 'partial' ? value : 'unknown';
514
+ }
515
+ async function runSearch(input, action, deps) {
516
+ const retrieval = await deps.search({
517
+ query: queryForInput(input, action.query),
518
+ entity_types: action.entity_types ?? [...DEFAULT_ENTITY_TYPES],
519
+ required_fields: action.required_fields,
520
+ ...(action.seed_ids ? { seed_ids: action.seed_ids } : {}),
521
+ });
522
+ if (retrieval.status !== 'ok')
523
+ return dependencyFailure(input, action, retrieval);
524
+ const data = isRecord(retrieval.data) ? retrieval.data : {};
525
+ const candidates = Array.isArray(data.candidates) ? data.candidates : [];
526
+ const { references, availableFields, summaries } = referencesFromCandidates(candidates);
527
+ const coverage = coverageOf(data.coverage);
528
+ // Empty is meaningful only when the host confirms complete coverage. A
529
+ // partial/unknown result remains incomplete so the host can re-plan.
530
+ const outcome = references.length === 0 && coverage === 'complete' ? 'empty' : 'incomplete';
531
+ return success(input, action, retrieval.scope.project_codes, {
532
+ outcome,
533
+ references,
534
+ available_fields: availableFields,
535
+ missing_fields: requiredMissing(action.required_fields, availableFields),
536
+ coverage,
537
+ body: summaries.length > 0 ? { candidates: summaries } : null,
538
+ });
539
+ }
540
+ async function runRead(input, action, deps) {
541
+ const retrieval = await deps.read({
542
+ entity_id: action.entity_id,
543
+ entity_type: action.entity_type,
544
+ required_fields: action.required_fields,
545
+ });
546
+ if (retrieval.status !== 'ok')
547
+ return dependencyFailure(input, action, retrieval);
548
+ const data = isRecord(retrieval.data) ? retrieval.data : {};
549
+ const raw = isRecord(data.entity) ? data.entity : null;
550
+ if (!raw) {
551
+ const coverage = coverageOf(data.coverage);
552
+ return success(input, action, retrieval.scope.project_codes, {
553
+ outcome: coverage === 'complete' ? 'empty' : 'incomplete',
554
+ coverage,
555
+ missing_fields: action.required_fields,
556
+ });
557
+ }
558
+ let projection;
559
+ try {
560
+ projection = deps.projectEntity(raw);
561
+ if (!isRecord(projection) || !isRecord(projection.body))
562
+ throw new Error('projectEntity must return a public body');
563
+ }
564
+ catch (error) {
565
+ return {
566
+ status: 'error',
567
+ scope: retrieval.scope,
568
+ data: baseResult(input, action, retrieval.scope.project_codes, {
569
+ outcome: 'unsupported',
570
+ coverage: 'unknown',
571
+ error_kind: 'brainbase_knowledge_lookup_projection_invalid',
572
+ }),
573
+ error: {
574
+ code: 'brainbase_knowledge_lookup_projection_invalid',
575
+ message: error instanceof Error ? error.message : 'projectEntity returned an invalid public projection',
576
+ },
577
+ };
578
+ }
579
+ const projected = publicProjection(raw, projection);
580
+ const reference = {
581
+ id: String(raw.id ?? raw.entity_id),
582
+ entity_type: projected.entityType,
583
+ evidence_fields: projected.fields,
584
+ };
585
+ const missingFields = requiredMissing(action.required_fields, projected.fields);
586
+ return success(input, action, retrieval.scope.project_codes, {
587
+ outcome: missingFields.length > 0 ? 'incomplete' : 'retrieved',
588
+ references: [reference],
589
+ available_fields: projected.fields,
590
+ missing_fields: missingFields,
591
+ coverage: coverageOf(data.coverage) === 'unknown' ? 'complete' : coverageOf(data.coverage),
592
+ body: projected.body,
593
+ });
594
+ }
595
+ async function runRelation(input, action, deps) {
596
+ const retrieval = await deps.followRelation({
597
+ query: queryForInput(input),
598
+ entity_types: action.target_types ?? [...DEFAULT_ENTITY_TYPES],
599
+ required_fields: action.required_fields,
600
+ seed_ids: action.seed_ids,
601
+ relation: action.relation,
602
+ direction: action.direction,
603
+ ...(action.target_types ? { target_types: action.target_types } : {}),
604
+ });
605
+ if (retrieval.status !== 'ok')
606
+ return dependencyFailure(input, action, retrieval);
607
+ const data = isRecord(retrieval.data) ? retrieval.data : {};
608
+ const candidates = Array.isArray(data.candidates) ? data.candidates : [];
609
+ const { references, availableFields, summaries } = referencesFromCandidates(candidates);
610
+ const coverage = coverageOf(data.coverage);
611
+ return success(input, action, retrieval.scope.project_codes, {
612
+ outcome: references.length === 0 && coverage === 'complete' ? 'empty' : 'incomplete',
613
+ references,
614
+ available_fields: availableFields,
615
+ missing_fields: requiredMissing(action.required_fields, availableFields),
616
+ coverage,
617
+ body: summaries.length > 0 ? { candidates: summaries } : null,
618
+ });
619
+ }
620
+ function runFinish(input, action) {
621
+ // A finish proposal is advisory. The host verifies cited fields against
622
+ // actual read attempts before accepting it; this branch performs no I/O.
623
+ return success(input, action, [], {
624
+ outcome: 'incomplete',
625
+ coverage: 'unknown',
626
+ proposed_finish: action.finish,
627
+ });
628
+ }
629
+ function isFinishAction(action) {
630
+ return action.kind === 'finish' && 'finish' in action;
631
+ }
632
+ function unsupported(input, action, error) {
633
+ const data = input && action
634
+ ? baseResult(input, action, [], { outcome: 'unsupported', coverage: 'unknown', error_kind: error.code })
635
+ : undefined;
636
+ return {
637
+ status: 'error',
638
+ scope: { project_codes: [] },
639
+ ...(data ? { data } : {}),
640
+ error: { code: error.code, message: error.message },
641
+ };
642
+ }
643
+ export const knowledgeLookupTools = [{
644
+ name: 'brainbase_knowledge_lookup',
645
+ description: 'Find and read authorized Graph knowledge by purpose. Each call performs one bounded search, read, relation follow, or finish proposal; project hints never change authorization scope.',
646
+ inputSchema: {
647
+ type: 'object',
648
+ properties: {
649
+ question: { type: 'string', minLength: 1, maxLength: MAX_TEXT_LENGTH },
650
+ target_hint: { type: 'string', minLength: 1, maxLength: MAX_HINT_LENGTH },
651
+ required_fields: {
652
+ type: 'array',
653
+ minItems: 1,
654
+ maxItems: MAX_FIELDS,
655
+ description: 'Public field paths only; use roots such as content, body, statement, markdown, summary, name, or environments.production.endpoint.',
656
+ items: { type: 'string', minLength: 1, maxLength: MAX_FIELD_LENGTH },
657
+ },
658
+ known_entity_id: { type: 'string', minLength: 1, maxLength: 1_000 },
659
+ context_hints: { type: 'array', maxItems: MAX_CONTEXT_HINTS, items: { type: 'string', minLength: 1, maxLength: MAX_HINT_LENGTH } },
660
+ lookup_id: { type: 'string', minLength: 1, maxLength: 200 },
661
+ revision: { type: 'integer', minimum: 0 },
662
+ attempt_id: { type: 'string', minLength: 1, maxLength: 300 },
663
+ assessment: { type: 'string', enum: ['sufficient', 'insufficient'] },
664
+ missing_information: { type: 'array', maxItems: MAX_FIELDS, items: { type: 'string', minLength: 1, maxLength: MAX_HINT_LENGTH } },
665
+ based_on_attempt_ids: { type: 'array', maxItems: MAX_IDS, items: { type: 'string', minLength: 1, maxLength: 300 } },
666
+ why_different: { type: 'string', minLength: 1, maxLength: MAX_HINT_LENGTH },
667
+ next_action: {
668
+ type: 'object',
669
+ properties: {
670
+ kind: { type: 'string', enum: ['search', 'read', 'follow_relation', 'finish'] },
671
+ query: { type: 'string', minLength: 1, maxLength: MAX_TEXT_LENGTH },
672
+ entity_types: { type: 'array', maxItems: 10, items: { type: 'string', minLength: 1, maxLength: 100 } },
673
+ seed_ids: { type: 'array', minItems: 1, maxItems: MAX_IDS, items: { type: 'string', minLength: 1, maxLength: 1_000 } },
674
+ entity_id: { type: 'string', minLength: 1, maxLength: 1_000 },
675
+ entity_type: { type: 'string', minLength: 1, maxLength: 100 },
676
+ required_fields: {
677
+ type: 'array',
678
+ minItems: 1,
679
+ maxItems: MAX_FIELDS,
680
+ description: 'Must contain the same public field paths as the top-level required_fields.',
681
+ items: { type: 'string', minLength: 1, maxLength: MAX_FIELD_LENGTH },
682
+ },
683
+ relation: { type: 'string', minLength: 1, maxLength: MAX_RELATION_LENGTH },
684
+ direction: { type: 'string', enum: ['incoming', 'outgoing'] },
685
+ target_types: { type: 'array', maxItems: 10, items: { type: 'string', minLength: 1, maxLength: 100 } },
686
+ why_different: { type: 'string', minLength: 1, maxLength: MAX_HINT_LENGTH },
687
+ assessment: { type: 'string', enum: ['sufficient', 'insufficient'] },
688
+ status: { type: 'string', enum: ['satisfied', 'unresolved', 'needs_user_input'] },
689
+ reference_ids: { type: 'array', maxItems: MAX_IDS, items: { type: 'string', minLength: 1, maxLength: 1_000 } },
690
+ field_evidence: {
691
+ type: 'array',
692
+ maxItems: MAX_FIELDS,
693
+ items: {
694
+ type: 'object',
695
+ properties: {
696
+ field: { type: 'string', minLength: 1, maxLength: MAX_FIELD_LENGTH },
697
+ reference_id: { type: 'string', minLength: 1, maxLength: 1_000 },
698
+ attempt_id: { type: 'string', minLength: 1, maxLength: 300 },
699
+ },
700
+ required: ['field', 'reference_id', 'attempt_id'],
701
+ additionalProperties: false,
702
+ },
703
+ },
704
+ unresolved_items: { type: 'array', maxItems: MAX_FIELDS, items: { type: 'string', minLength: 1, maxLength: MAX_HINT_LENGTH } },
705
+ termination_reason: { type: 'string', minLength: 1, maxLength: MAX_HINT_LENGTH },
706
+ },
707
+ oneOf: [
708
+ { properties: { kind: { const: 'search' } }, required: ['kind'] },
709
+ { properties: { kind: { const: 'read' } }, required: ['kind', 'entity_id', 'entity_type'] },
710
+ { properties: { kind: { const: 'follow_relation' } }, required: ['kind', 'seed_ids', 'relation', 'direction'] },
711
+ {
712
+ properties: { kind: { const: 'finish' } },
713
+ required: ['kind', 'assessment', 'status', 'reference_ids', 'field_evidence', 'unresolved_items', 'termination_reason'],
714
+ },
715
+ ],
716
+ required: ['kind'],
717
+ additionalProperties: false,
718
+ },
719
+ },
720
+ required: ['question', 'target_hint', 'required_fields'],
721
+ additionalProperties: false,
722
+ },
723
+ }];
724
+ export async function handleKnowledgeLookupToolCall(name, args, dependencies) {
725
+ if (name !== 'brainbase_knowledge_lookup')
726
+ return null;
727
+ let input = null;
728
+ let action = null;
729
+ try {
730
+ // A project code is deliberately not part of this purpose-based contract.
731
+ // Silently accepting it would make a model supplied hint look like an
732
+ // authorization filter.
733
+ if (Object.hasOwn(args, 'project_code')) {
734
+ throw new LookupInputError('brainbase_knowledge_lookup_input_invalid', 'project_code is not accepted; put a project hint in target_hint or context_hints');
735
+ }
736
+ input = commonInput(args);
737
+ action = normalizeAction(args.next_action, input);
738
+ if (isFinishAction(action))
739
+ return runFinish(input, action);
740
+ if (action.kind === 'search')
741
+ return await runSearch(input, action, dependencies);
742
+ if (action.kind === 'read')
743
+ return await runRead(input, action, dependencies);
744
+ return await runRelation(input, action, dependencies);
745
+ }
746
+ catch (error) {
747
+ if (error instanceof LookupInputError)
748
+ return unsupported(input, action, error);
749
+ return {
750
+ status: 'unavailable',
751
+ scope: { project_codes: [] },
752
+ error: {
753
+ code: 'brainbase_knowledge_lookup_unavailable',
754
+ message: error instanceof Error ? error.message : String(error),
755
+ },
756
+ };
757
+ }
758
+ }