@carlonicora/nestjs-neo4jsonapi 2.1.0 → 2.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.
Files changed (144) hide show
  1. package/dist/agents/graph/graph.module.d.ts.map +1 -1
  2. package/dist/agents/graph/graph.module.js +3 -0
  3. package/dist/agents/graph/graph.module.js.map +1 -1
  4. package/dist/agents/graph/interfaces/graph.catalog.interface.d.ts +24 -0
  5. package/dist/agents/graph/interfaces/graph.catalog.interface.d.ts.map +1 -1
  6. package/dist/agents/graph/services/graph.catalog.service.d.ts +9 -0
  7. package/dist/agents/graph/services/graph.catalog.service.d.ts.map +1 -1
  8. package/dist/agents/graph/services/graph.catalog.service.js +61 -1
  9. package/dist/agents/graph/services/graph.catalog.service.js.map +1 -1
  10. package/dist/agents/graph/services/graph.search.service.d.ts +16 -1
  11. package/dist/agents/graph/services/graph.search.service.d.ts.map +1 -1
  12. package/dist/agents/graph/services/graph.search.service.js +51 -4
  13. package/dist/agents/graph/services/graph.search.service.js.map +1 -1
  14. package/dist/agents/graph/services/humanize-tool.d.ts +17 -2
  15. package/dist/agents/graph/services/humanize-tool.d.ts.map +1 -1
  16. package/dist/agents/graph/services/humanize-tool.js +71 -6
  17. package/dist/agents/graph/services/humanize-tool.js.map +1 -1
  18. package/dist/agents/graph/services/materialise-bridge.d.ts +6 -0
  19. package/dist/agents/graph/services/materialise-bridge.d.ts.map +1 -1
  20. package/dist/agents/graph/services/materialise-bridge.js +17 -1
  21. package/dist/agents/graph/services/materialise-bridge.js.map +1 -1
  22. package/dist/agents/graph/services/scope.guard.d.ts +43 -0
  23. package/dist/agents/graph/services/scope.guard.d.ts.map +1 -0
  24. package/dist/agents/graph/services/scope.guard.js +91 -0
  25. package/dist/agents/graph/services/scope.guard.js.map +1 -0
  26. package/dist/agents/graph/tools/read-entity.tool.d.ts +9 -1
  27. package/dist/agents/graph/tools/read-entity.tool.d.ts.map +1 -1
  28. package/dist/agents/graph/tools/read-entity.tool.js +30 -5
  29. package/dist/agents/graph/tools/read-entity.tool.js.map +1 -1
  30. package/dist/agents/graph/tools/resolve-entity.tool.d.ts.map +1 -1
  31. package/dist/agents/graph/tools/resolve-entity.tool.js +8 -1
  32. package/dist/agents/graph/tools/resolve-entity.tool.js.map +1 -1
  33. package/dist/agents/graph/tools/search-entities.tool.d.ts +9 -1
  34. package/dist/agents/graph/tools/search-entities.tool.d.ts.map +1 -1
  35. package/dist/agents/graph/tools/search-entities.tool.js +37 -8
  36. package/dist/agents/graph/tools/search-entities.tool.js.map +1 -1
  37. package/dist/agents/graph/tools/tool.factory.d.ts +4 -0
  38. package/dist/agents/graph/tools/tool.factory.d.ts.map +1 -1
  39. package/dist/agents/graph/tools/tool.factory.js.map +1 -1
  40. package/dist/agents/graph/tools/traverse.tool.d.ts +9 -1
  41. package/dist/agents/graph/tools/traverse.tool.d.ts.map +1 -1
  42. package/dist/agents/graph/tools/traverse.tool.js +21 -5
  43. package/dist/agents/graph/tools/traverse.tool.js.map +1 -1
  44. package/dist/agents/operator/contexts/operator.context.d.ts +4 -0
  45. package/dist/agents/operator/contexts/operator.context.d.ts.map +1 -1
  46. package/dist/agents/operator/contexts/operator.context.js +4 -0
  47. package/dist/agents/operator/contexts/operator.context.js.map +1 -1
  48. package/dist/agents/operator/operator.module.d.ts.map +1 -1
  49. package/dist/agents/operator/operator.module.js +4 -0
  50. package/dist/agents/operator/operator.module.js.map +1 -1
  51. package/dist/agents/operator/services/operator.service.d.ts +15 -0
  52. package/dist/agents/operator/services/operator.service.d.ts.map +1 -1
  53. package/dist/agents/operator/services/operator.service.js +23 -5
  54. package/dist/agents/operator/services/operator.service.js.map +1 -1
  55. package/dist/agents/operator/tools/entity-write.tools.d.ts +124 -0
  56. package/dist/agents/operator/tools/entity-write.tools.d.ts.map +1 -0
  57. package/dist/agents/operator/tools/entity-write.tools.js +471 -0
  58. package/dist/agents/operator/tools/entity-write.tools.js.map +1 -0
  59. package/dist/agents/operator/tools/operator.tool.registry.d.ts +5 -1
  60. package/dist/agents/operator/tools/operator.tool.registry.d.ts.map +1 -1
  61. package/dist/agents/operator/tools/operator.tool.registry.js +19 -2
  62. package/dist/agents/operator/tools/operator.tool.registry.js.map +1 -1
  63. package/dist/agents/operator/tools/search-documents.tool.d.ts +16 -1
  64. package/dist/agents/operator/tools/search-documents.tool.d.ts.map +1 -1
  65. package/dist/agents/operator/tools/search-documents.tool.js +59 -3
  66. package/dist/agents/operator/tools/search-documents.tool.js.map +1 -1
  67. package/dist/agents/prompts/index.d.ts +1 -0
  68. package/dist/agents/prompts/index.d.ts.map +1 -1
  69. package/dist/agents/prompts/index.js +7 -1
  70. package/dist/agents/prompts/index.js.map +1 -1
  71. package/dist/agents/responder/contexts/responder.context.d.ts +19 -0
  72. package/dist/agents/responder/contexts/responder.context.d.ts.map +1 -1
  73. package/dist/agents/responder/contexts/responder.context.js +6 -0
  74. package/dist/agents/responder/contexts/responder.context.js.map +1 -1
  75. package/dist/agents/responder/factories/responder.context.factory.d.ts +4 -0
  76. package/dist/agents/responder/factories/responder.context.factory.d.ts.map +1 -1
  77. package/dist/agents/responder/factories/responder.context.factory.js +2 -0
  78. package/dist/agents/responder/factories/responder.context.factory.js.map +1 -1
  79. package/dist/agents/responder/nodes/graph.node.service.d.ts.map +1 -1
  80. package/dist/agents/responder/nodes/graph.node.service.js +18 -2
  81. package/dist/agents/responder/nodes/graph.node.service.js.map +1 -1
  82. package/dist/agents/responder/nodes/planner.node.service.d.ts +1 -1
  83. package/dist/agents/responder/nodes/planner.node.service.d.ts.map +1 -1
  84. package/dist/agents/responder/nodes/planner.node.service.js +5 -0
  85. package/dist/agents/responder/nodes/planner.node.service.js.map +1 -1
  86. package/dist/agents/responder/nodes/responder.answer.node.service.d.ts +33 -1
  87. package/dist/agents/responder/nodes/responder.answer.node.service.d.ts.map +1 -1
  88. package/dist/agents/responder/nodes/responder.answer.node.service.js +69 -27
  89. package/dist/agents/responder/nodes/responder.answer.node.service.js.map +1 -1
  90. package/dist/agents/responder/services/responder.service.d.ts +7 -0
  91. package/dist/agents/responder/services/responder.service.d.ts.map +1 -1
  92. package/dist/agents/responder/services/responder.service.js +3 -0
  93. package/dist/agents/responder/services/responder.service.js.map +1 -1
  94. package/dist/common/interfaces/entity.schema.interface.d.ts +16 -0
  95. package/dist/common/interfaces/entity.schema.interface.d.ts.map +1 -1
  96. package/dist/common/interfaces/index.d.ts +1 -0
  97. package/dist/common/interfaces/index.d.ts.map +1 -1
  98. package/dist/common/interfaces/index.js +1 -0
  99. package/dist/common/interfaces/index.js.map +1 -1
  100. package/dist/common/interfaces/seed.context.interface.d.ts +62 -0
  101. package/dist/common/interfaces/seed.context.interface.d.ts.map +1 -0
  102. package/dist/common/interfaces/seed.context.interface.js +20 -0
  103. package/dist/common/interfaces/seed.context.interface.js.map +1 -0
  104. package/dist/config/interfaces/config.prompts.interface.d.ts +12 -0
  105. package/dist/config/interfaces/config.prompts.interface.d.ts.map +1 -1
  106. package/dist/core/blocknote/services/blocknote.service.d.ts +23 -14
  107. package/dist/core/blocknote/services/blocknote.service.d.ts.map +1 -1
  108. package/dist/core/blocknote/services/blocknote.service.js +48 -32
  109. package/dist/core/blocknote/services/blocknote.service.js.map +1 -1
  110. package/dist/core/llm/services/llm.service.d.ts.map +1 -1
  111. package/dist/core/llm/services/llm.service.js +34 -2
  112. package/dist/core/llm/services/llm.service.js.map +1 -1
  113. package/dist/core/llm/services/tool-error-feedback.d.ts +40 -0
  114. package/dist/core/llm/services/tool-error-feedback.d.ts.map +1 -0
  115. package/dist/core/llm/services/tool-error-feedback.js +131 -0
  116. package/dist/core/llm/services/tool-error-feedback.js.map +1 -0
  117. package/dist/foundations/assistant/assistant.module.d.ts.map +1 -1
  118. package/dist/foundations/assistant/assistant.module.js +7 -1
  119. package/dist/foundations/assistant/assistant.module.js.map +1 -1
  120. package/dist/foundations/assistant/controllers/assistant.controller.d.ts +13 -1
  121. package/dist/foundations/assistant/controllers/assistant.controller.d.ts.map +1 -1
  122. package/dist/foundations/assistant/controllers/assistant.controller.js +37 -3
  123. package/dist/foundations/assistant/controllers/assistant.controller.js.map +1 -1
  124. package/dist/foundations/assistant/dtos/assistant-append.dto.d.ts +6 -0
  125. package/dist/foundations/assistant/dtos/assistant-append.dto.d.ts.map +1 -1
  126. package/dist/foundations/assistant/dtos/assistant-append.dto.js +1 -1
  127. package/dist/foundations/assistant/dtos/assistant-append.dto.js.map +1 -1
  128. package/dist/foundations/assistant/dtos/assistant-post.dto.d.ts +29 -0
  129. package/dist/foundations/assistant/dtos/assistant-post.dto.d.ts.map +1 -1
  130. package/dist/foundations/assistant/dtos/assistant-post.dto.js +47 -2
  131. package/dist/foundations/assistant/dtos/assistant-post.dto.js.map +1 -1
  132. package/dist/foundations/assistant/repositories/assistant.repository.d.ts +22 -0
  133. package/dist/foundations/assistant/repositories/assistant.repository.d.ts.map +1 -1
  134. package/dist/foundations/assistant/repositories/assistant.repository.js +28 -0
  135. package/dist/foundations/assistant/repositories/assistant.repository.js.map +1 -1
  136. package/dist/foundations/assistant/services/assistant-mention.extractor.d.ts +36 -0
  137. package/dist/foundations/assistant/services/assistant-mention.extractor.d.ts.map +1 -0
  138. package/dist/foundations/assistant/services/assistant-mention.extractor.js +99 -0
  139. package/dist/foundations/assistant/services/assistant-mention.extractor.js.map +1 -0
  140. package/dist/foundations/assistant/services/assistant.service.d.ts +93 -2
  141. package/dist/foundations/assistant/services/assistant.service.d.ts.map +1 -1
  142. package/dist/foundations/assistant/services/assistant.service.js +388 -60
  143. package/dist/foundations/assistant/services/assistant.service.js.map +1 -1
  144. package/package.json +1 -1
@@ -8,6 +8,9 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
8
8
  var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
10
  };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
11
14
  var AssistantService_1;
12
15
  Object.defineProperty(exports, "__esModule", { value: true });
13
16
  exports.AssistantService = exports.MAX_MESSAGES_TO_LLM = void 0;
@@ -19,9 +22,13 @@ const operator_checkpointer_service_1 = require("../../../agents/operator/servic
19
22
  const operator_service_1 = require("../../../agents/operator/services/operator.service");
20
23
  const responder_service_1 = require("../../../agents/responder/services/responder.service");
21
24
  const graph_catalog_service_1 = require("../../../agents/graph/services/graph.catalog.service");
25
+ const scope_guard_1 = require("../../../agents/graph/services/scope.guard");
22
26
  const user_modules_repository_1 = require("../../../agents/graph/repositories/user-modules.repository");
23
27
  const agentmessage_type_1 = require("../../../common/enums/agentmessage.type");
28
+ const seed_context_interface_1 = require("../../../common/interfaces/seed.context.interface");
24
29
  const entity_service_registry_1 = require("../../../common/registries/entity.service.registry");
30
+ const blocknote_service_1 = require("../../../core/blocknote/services/blocknote.service");
31
+ const jsonapi_paginator_1 = require("../../../core/jsonapi/serialisers/jsonapi.paginator");
25
32
  const jsonapi_service_1 = require("../../../core/jsonapi/services/jsonapi.service");
26
33
  const abstract_service_1 = require("../../../core/neo4j/abstracts/abstract.service");
27
34
  const websocket_service_1 = require("../../../core/websocket/services/websocket.service");
@@ -33,7 +40,9 @@ const assistant_message_repository_1 = require("../../assistant-message/reposito
33
40
  const assistant_message_service_1 = require("../../assistant-message/services/assistant-message.service");
34
41
  const assistant_1 = require("../entities/assistant");
35
42
  const assistant_meta_1 = require("../entities/assistant.meta");
43
+ const registry_1 = require("../../../common/registries/registry");
36
44
  const assistant_repository_1 = require("../repositories/assistant.repository");
45
+ const assistant_mention_extractor_1 = require("./assistant-mention.extractor");
37
46
  /**
38
47
  * Maximum number of prior messages (turns) passed to the LLM on each turn.
39
48
  * Keeps prompt size and cost bounded for long conversations.
@@ -55,7 +64,7 @@ exports.MAX_MESSAGES_TO_LLM = 20;
55
64
  * AssistantMessageRepository.linkReferences).
56
65
  */
57
66
  let AssistantService = AssistantService_1 = class AssistantService extends abstract_service_1.AbstractService {
58
- constructor(jsonApiService, assistantRepository, clsService, userModuleIdsRepository, responder, assistantMessages, assistantMessageRepo, graphCatalog, entityServices, operator, assistantActions, assistantActionRepo, webSocketService, configService) {
67
+ constructor(jsonApiService, assistantRepository, clsService, userModuleIdsRepository, responder, assistantMessages, assistantMessageRepo, graphCatalog, entityServices, operator, assistantActions, assistantActionRepo, webSocketService, configService, mentions, blockNote, scopeGuard, seedContextProviders) {
59
68
  super(jsonApiService, assistantRepository, clsService, assistant_1.AssistantDescriptor.model);
60
69
  this.userModuleIdsRepository = userModuleIdsRepository;
61
70
  this.responder = responder;
@@ -68,15 +77,31 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
68
77
  this.assistantActionRepo = assistantActionRepo;
69
78
  this.webSocketService = webSocketService;
70
79
  this.configService = configService;
80
+ this.mentions = mentions;
81
+ this.blockNote = blockNote;
82
+ this.scopeGuard = scopeGuard;
83
+ this.seedContextProviders = seedContextProviders;
71
84
  this.descriptor = assistant_1.AssistantDescriptor;
72
85
  this.assistantLogger = new common_1.Logger(AssistantService_1.name);
73
86
  }
74
87
  async createWithFirstMessage(params) {
75
88
  const userModuleIds = await this.userModuleIdsRepository.findModuleIdsForUser(params.userId);
76
- const title = params.title?.trim() || this.autoTitle(params.firstMessage);
89
+ const ctx = this.buildTurnContext({
90
+ companyId: params.companyId,
91
+ userId: params.userId,
92
+ userModuleIds,
93
+ boundContent: params.boundContent,
94
+ });
95
+ const { content: firstMessage, plainText, pinned, } = await this.resolveUserTurnInput({
96
+ rawContent: params.firstMessage,
97
+ ctx,
98
+ });
99
+ const title = params.title?.trim() || this.autoTitle(plainText);
77
100
  const assistantId = (0, crypto_1.randomUUID)();
78
101
  const userMessageId = (0, crypto_1.randomUUID)();
79
102
  // 1. Create the Assistant (owner edge attached from CLS via contextKey).
103
+ // A bound thread also gets the BOUND_TO edge to the resource it is
104
+ // confined to — the relationship is immutable, so it is set on create.
80
105
  await this.createFromDTO({
81
106
  data: {
82
107
  type: assistant_meta_1.assistantMeta.type,
@@ -84,6 +109,7 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
84
109
  attributes: { title },
85
110
  },
86
111
  });
112
+ await this.attachBoundContent(assistantId, params.boundContent);
87
113
  // 2. Create the first user message at position 0.
88
114
  await this.assistantMessages.createFromDTO({
89
115
  data: {
@@ -91,7 +117,7 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
91
117
  id: userMessageId,
92
118
  attributes: {
93
119
  role: "user",
94
- content: params.firstMessage,
120
+ content: firstMessage,
95
121
  position: 0,
96
122
  },
97
123
  relationships: {
@@ -99,15 +125,16 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
99
125
  },
100
126
  },
101
127
  });
128
+ await this.linkPinnedMentions({ messageId: userMessageId, pinned });
102
129
  const userMessage = await this.assistantMessageRepo.findById({ id: userMessageId });
103
130
  // 3. Run the agent turn using the just-created user message as context.
104
131
  const turn = await this.runAgentTurn({
105
- companyId: params.companyId,
106
- userId: params.userId,
107
- userModuleIds,
132
+ ctx,
108
133
  priorMessages: [],
109
- newUserMessage: { role: "user", content: params.firstMessage },
134
+ newUserMessage: { role: "user", content: firstMessage },
135
+ pinned,
110
136
  assistantId,
137
+ contentScope: this.toContentScope(params.boundContent),
111
138
  howToMode: params.howToMode,
112
139
  limitToHowToId: params.limitToHowToId,
113
140
  });
@@ -156,6 +183,18 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
156
183
  // Verify ownership via the owner-RBAC-enforcing findById.
157
184
  await this.repository.findById({ id: params.assistantId });
158
185
  const userModuleIds = await this.userModuleIdsRepository.findModuleIdsForUser(params.userId);
186
+ // An append inherits the thread's binding — never a client-supplied one.
187
+ const contentScope = await this.resolveContentScope(params.assistantId);
188
+ const ctx = this.buildTurnContext({
189
+ companyId: params.companyId,
190
+ userId: params.userId,
191
+ userModuleIds,
192
+ boundContent: this.toBoundContent(contentScope),
193
+ });
194
+ const { content: newMessage, pinned } = await this.resolveUserTurnInput({
195
+ rawContent: params.newMessage,
196
+ ctx,
197
+ });
159
198
  // Load prior messages for agent context.
160
199
  const priorMessages = await this.loadRecentMessages({
161
200
  assistantId: params.assistantId,
@@ -171,7 +210,7 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
171
210
  id: userMessageId,
172
211
  attributes: {
173
212
  role: "user",
174
- content: params.newMessage,
213
+ content: newMessage,
175
214
  position: nextPosition,
176
215
  },
177
216
  relationships: {
@@ -179,14 +218,15 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
179
218
  },
180
219
  },
181
220
  });
221
+ await this.linkPinnedMentions({ messageId: userMessageId, pinned });
182
222
  const userMessage = await this.assistantMessageRepo.findById({ id: userMessageId });
183
223
  const turn = await this.runAgentTurn({
184
- companyId: params.companyId,
185
- userId: params.userId,
186
- userModuleIds,
224
+ ctx,
187
225
  priorMessages,
188
- newUserMessage: { role: "user", content: params.newMessage },
226
+ newUserMessage: { role: "user", content: newMessage },
227
+ pinned,
189
228
  assistantId: params.assistantId,
229
+ contentScope,
190
230
  howToMode: params.howToMode,
191
231
  limitToHowToId: params.limitToHowToId,
192
232
  });
@@ -237,7 +277,17 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
237
277
  */
238
278
  async createWithFirstMessageOperator(params) {
239
279
  const userModuleIds = await this.userModuleIdsRepository.findModuleIdsForUser(params.userId);
240
- const title = params.title?.trim() || this.autoTitle(params.firstMessage);
280
+ const ctx = this.buildTurnContext({
281
+ companyId: params.companyId,
282
+ userId: params.userId,
283
+ userModuleIds,
284
+ boundContent: params.boundContent,
285
+ });
286
+ const { content: firstMessage, plainText, pinned, } = await this.resolveUserTurnInput({
287
+ rawContent: params.firstMessage,
288
+ ctx,
289
+ });
290
+ const title = params.title?.trim() || this.autoTitle(plainText);
241
291
  const assistantId = (0, crypto_1.randomUUID)();
242
292
  const userMessageId = (0, crypto_1.randomUUID)();
243
293
  // Persist the engine marker so clients can route follow-up turns to the
@@ -247,6 +297,13 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
247
297
  type: assistant_meta_1.assistantMeta.type,
248
298
  id: assistantId,
249
299
  attributes: { title, engine: "operator" },
300
+ ...(params.boundContent
301
+ ? {
302
+ relationships: {
303
+ content: { data: { type: params.boundContent.type, id: params.boundContent.id } },
304
+ },
305
+ }
306
+ : {}),
250
307
  },
251
308
  });
252
309
  await this.assistantMessages.createFromDTO({
@@ -255,7 +312,7 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
255
312
  id: userMessageId,
256
313
  attributes: {
257
314
  role: "user",
258
- content: params.firstMessage,
315
+ content: firstMessage,
259
316
  position: 0,
260
317
  },
261
318
  relationships: {
@@ -263,18 +320,20 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
263
320
  },
264
321
  },
265
322
  });
323
+ await this.linkPinnedMentions({ messageId: userMessageId, pinned });
266
324
  const userMessage = await this.assistantMessageRepo.findById({ id: userMessageId });
267
325
  const threadId = `${assistantId}:${userMessageId}`;
268
- const contentScope = await this.resolveContentScope(assistantId);
326
+ const contentScope = this.toContentScope(params.boundContent);
269
327
  const result = await this.runOperatorTurn({
270
- companyId: params.companyId,
271
- userId: params.userId,
272
- userModuleIds,
328
+ ctx,
273
329
  priorMessages: [],
274
- question: params.firstMessage,
330
+ question: firstMessage,
331
+ pinned,
275
332
  assistantId,
276
333
  threadId,
277
334
  contentScope,
335
+ howToMode: params.howToMode,
336
+ limitToHowToId: params.limitToHowToId,
278
337
  });
279
338
  const outcome = await this.persistOperatorOutcome({
280
339
  assistantId,
@@ -296,6 +355,17 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
296
355
  // Verify ownership via the owner-RBAC-enforcing findById.
297
356
  await this.repository.findById({ id: params.assistantId });
298
357
  const userModuleIds = await this.userModuleIdsRepository.findModuleIdsForUser(params.userId);
358
+ const contentScope = await this.resolveContentScope(params.assistantId);
359
+ const ctx = this.buildTurnContext({
360
+ companyId: params.companyId,
361
+ userId: params.userId,
362
+ userModuleIds,
363
+ boundContent: this.toBoundContent(contentScope),
364
+ });
365
+ const { content: newMessage, pinned } = await this.resolveUserTurnInput({
366
+ rawContent: params.newMessage,
367
+ ctx,
368
+ });
299
369
  const priorMessages = await this.loadRecentMessages({
300
370
  assistantId: params.assistantId,
301
371
  limit: exports.MAX_MESSAGES_TO_LLM,
@@ -310,7 +380,7 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
310
380
  id: userMessageId,
311
381
  attributes: {
312
382
  role: "user",
313
- content: params.newMessage,
383
+ content: newMessage,
314
384
  position: nextPosition,
315
385
  },
316
386
  relationships: {
@@ -318,18 +388,19 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
318
388
  },
319
389
  },
320
390
  });
391
+ await this.linkPinnedMentions({ messageId: userMessageId, pinned });
321
392
  const userMessage = await this.assistantMessageRepo.findById({ id: userMessageId });
322
393
  const threadId = `${params.assistantId}:${userMessageId}`;
323
- const contentScope = await this.resolveContentScope(params.assistantId);
324
394
  const result = await this.runOperatorTurn({
325
- companyId: params.companyId,
326
- userId: params.userId,
327
- userModuleIds,
395
+ ctx,
328
396
  priorMessages,
329
- question: params.newMessage,
397
+ question: newMessage,
398
+ pinned,
330
399
  assistantId: params.assistantId,
331
400
  threadId,
332
401
  contentScope,
402
+ howToMode: params.howToMode,
403
+ limitToHowToId: params.limitToHowToId,
333
404
  });
334
405
  const outcome = await this.persistOperatorOutcome({
335
406
  assistantId: params.assistantId,
@@ -387,6 +458,19 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
387
458
  type: this.roleToType(m.role),
388
459
  content: m.content,
389
460
  }));
461
+ // Rebuild the same turn context appendMessageOperator would build, so the
462
+ // resumed run gets the scope AND the seed contexts the original run had.
463
+ const resumeCtx = this.buildTurnContext({
464
+ companyId,
465
+ userId,
466
+ userModuleIds,
467
+ boundContent: this.toBoundContent(contentScope ?? {}),
468
+ });
469
+ const seedContexts = await this.collectSeedContexts({
470
+ ctx: resumeCtx,
471
+ contentScope: contentScope ?? {},
472
+ question: "",
473
+ });
390
474
  let result;
391
475
  try {
392
476
  result = await this.operator.resume({
@@ -397,6 +481,9 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
397
481
  userModuleIds,
398
482
  contentId: contentScope?.contentId,
399
483
  contentType: contentScope?.contentType,
484
+ scopeId: resumeCtx.scopeId,
485
+ scopeType: resumeCtx.scopeType,
486
+ seedContexts,
400
487
  messages,
401
488
  });
402
489
  }
@@ -447,9 +534,220 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
447
534
  // pending action supersedes the (now executed) original in the response.
448
535
  return { assistantMessage: outcome.assistantMessage, action: outcome.action ?? updatedAction };
449
536
  }
537
+ /**
538
+ * List the threads bound to one resource (e.g. every assistant thread for a
539
+ * campaign). Delegates to the inherited `findByRelated` over the `content`
540
+ * relationship — the BOUND_TO traversal already exists, so there is no new
541
+ * Cypher here and company/owner RBAC still comes from the repository.
542
+ */
543
+ async findByBoundContent(params) {
544
+ const model = registry_1.modelRegistry.getByType(params.boundType);
545
+ if (!model) {
546
+ throw new common_1.BadRequestException(`Unknown resource type "${params.boundType}" for boundType.`);
547
+ }
548
+ const paginator = new jsonapi_paginator_1.JsonApiPaginator(params.query);
549
+ // `findByRelatedEdge`, not `findByRelated`: the latter derives the target
550
+ // label from `relationships.content.model`, which for this polymorphic edge
551
+ // is the `assistantMeta` placeholder, so it would look for
552
+ // `(:Assistant { id: <campaign-uuid> })` and always return nothing. This
553
+ // variant takes the edge spec explicitly, with the label resolved from the
554
+ // model registry.
555
+ const data = await this.repository.findByRelatedEdge({
556
+ cypherLabel: "BOUND_TO",
557
+ cypherDirection: "out",
558
+ relatedLabel: model.labelName,
559
+ relatedId: params.boundId,
560
+ cursor: paginator.generateCursor(),
561
+ });
562
+ return this.jsonApiService.buildList(assistant_1.AssistantDescriptor.model, data, paginator);
563
+ }
564
+ /**
565
+ * Build the run's UserContext once per turn. `scopeId`/`scopeType` are set
566
+ * only when the thread is bound to a catalogued scope ROOT (a descriptor
567
+ * whose compiled scope chain is empty, e.g. a campaign). A thread bound to
568
+ * something further down a chain still gets content scope, but not a
569
+ * transitive run scope — the guard has no root to confine the run to.
570
+ */
571
+ buildTurnContext(params) {
572
+ const ctx = {
573
+ companyId: params.companyId,
574
+ userId: params.userId,
575
+ userModuleIds: params.userModuleIds,
576
+ };
577
+ const bound = params.boundContent;
578
+ if (!bound?.type || !bound?.id)
579
+ return ctx;
580
+ const scope = this.graphCatalog.getEntityDetail(bound.type, params.userModuleIds)?.scope;
581
+ if (scope && scope.path.length === 0 && scope.rootType === bound.type) {
582
+ ctx.scopeId = bound.id;
583
+ ctx.scopeType = bound.type;
584
+ }
585
+ else {
586
+ this.assistantLogger.warn(`buildTurnContext: bound type "${bound.type}" is not a catalogued scope root — the turn runs unscoped.`);
587
+ }
588
+ return ctx;
589
+ }
590
+ /**
591
+ * Collects seed-context blocks from the app-registered providers for one
592
+ * turn. Help-mode turns are never seeded. A provider returning null
593
+ * contributes nothing; a provider that throws is logged and skipped — the
594
+ * turn must run (unseeded) no matter what a provider does.
595
+ */
596
+ async collectSeedContexts(params) {
597
+ if (!this.seedContextProviders?.length)
598
+ return [];
599
+ if (params.howToMode || params.limitToHowToId)
600
+ return [];
601
+ const collected = [];
602
+ for (const provider of this.seedContextProviders) {
603
+ try {
604
+ const seed = await provider.provide({
605
+ companyId: params.ctx.companyId,
606
+ userId: params.ctx.userId,
607
+ userModuleIds: params.ctx.userModuleIds,
608
+ scopeId: params.ctx.scopeId,
609
+ scopeType: params.ctx.scopeType,
610
+ contentId: params.contentScope.contentId,
611
+ contentType: params.contentScope.contentType,
612
+ question: params.question,
613
+ });
614
+ if (seed)
615
+ collected.push(seed);
616
+ }
617
+ catch (err) {
618
+ this.assistantLogger.warn(`seed context provider failed — skipped: ${err instanceof Error ? err.message : String(err)}`);
619
+ }
620
+ }
621
+ return collected;
622
+ }
623
+ /**
624
+ * Write the `BOUND_TO` edge that scopes a thread to a resource.
625
+ *
626
+ * Deliberately NOT part of the `createFromDTO` call above: `BOUND_TO` is
627
+ * polymorphic, so its descriptor entry carries `assistantMeta` as a
628
+ * placeholder model. The generic create path reads `rel.model.labelName` to
629
+ * validate the target and to build the edge, so a Campaign id sent that way
630
+ * is looked up as `(:Assistant { id: … })` and 400s with "One or more
631
+ * related nodes do not exist."
632
+ *
633
+ * The label is resolved from the model registry — the same registry the
634
+ * descriptor's own polymorphic discriminator uses on the read path — so an
635
+ * unregistered type fails loudly here rather than writing a dangling edge.
636
+ */
637
+ async attachBoundContent(assistantId, boundContent) {
638
+ if (!boundContent)
639
+ return;
640
+ const model = registry_1.modelRegistry.getByType(boundContent.type);
641
+ if (!model) {
642
+ throw new common_1.BadRequestException(`Unknown resource type "${boundContent.type}" for the assistant's bound content.`);
643
+ }
644
+ await this.repository.bindContent({
645
+ assistantId,
646
+ targetLabel: model.labelName,
647
+ targetId: boundContent.id,
648
+ });
649
+ }
650
+ toContentScope(boundContent) {
651
+ return boundContent ? { contentId: boundContent.id, contentType: boundContent.type } : {};
652
+ }
653
+ toBoundContent(contentScope) {
654
+ return contentScope.contentId && contentScope.contentType
655
+ ? { type: contentScope.contentType, id: contentScope.contentId }
656
+ : undefined;
657
+ }
658
+ /**
659
+ * A BlockNote-backed field arrives as a JSON string in its own string
660
+ * attribute — the convention every host-app entity already uses (e.g.
661
+ * `Npc.description`: `JSON.stringify` out, `JSON.parse` back). Detect that
662
+ * shape without trusting it: a plain-text message must never be mistaken for
663
+ * a document, so this requires an array whose first entry is an object with
664
+ * a string `type`, which is the minimal shape of a BlockNote block.
665
+ */
666
+ parseBlockNoteDocument(raw) {
667
+ const trimmed = raw.trimStart();
668
+ if (!trimmed.startsWith("["))
669
+ return null;
670
+ try {
671
+ const parsed = JSON.parse(trimmed);
672
+ if (!Array.isArray(parsed) || parsed.length === 0)
673
+ return null;
674
+ const first = parsed[0];
675
+ if (!first || typeof first !== "object" || typeof first.type !== "string")
676
+ return null;
677
+ return parsed;
678
+ }
679
+ catch {
680
+ return null;
681
+ }
682
+ }
683
+ /**
684
+ * Derive from a user message: the markdown to persist (mention links kept, so
685
+ * the thread can re-render them), a plain-text form for the thread title, and
686
+ * the validated entity mentions to pin into the turn's focus context.
687
+ *
688
+ * Plain-text input passes straight through, so apps without a rich composer
689
+ * are unaffected.
690
+ */
691
+ async resolveUserTurnInput(params) {
692
+ const nodes = this.parseBlockNoteDocument(params.rawContent);
693
+ if (!nodes)
694
+ return { content: params.rawContent, plainText: params.rawContent, pinned: [] };
695
+ const content = this.blockNote.convertToMarkdown({ nodes, preserveMentions: true });
696
+ // Alias-only rendering: what a human reads. Used for the thread title so it
697
+ // does not show raw `[Name](mention://npcs/<uuid>)` markup.
698
+ const plainText = this.blockNote.convertToMarkdown({ nodes });
699
+ const pinned = await this.mentions.validate({
700
+ mentions: this.mentions.extract(nodes),
701
+ ctx: params.ctx,
702
+ });
703
+ return { content, plainText, pinned };
704
+ }
705
+ /**
706
+ * Materialise REFERENCES edges from the USER message to the entities that
707
+ * message named. The reference metadata is fixed: a mention is an explicit,
708
+ * maximally relevant pointer, not a ranked retrieval hit.
709
+ */
710
+ async linkPinnedMentions(params) {
711
+ if (!params.pinned.length)
712
+ return;
713
+ const references = params.pinned.map((mention) => ({
714
+ type: mention.type,
715
+ id: mention.id,
716
+ relevance: 100,
717
+ reason: "Named explicitly by the user in this message.",
718
+ }));
719
+ await this.assistantMessageRepo.linkReferences({ messageId: params.messageId, references });
720
+ }
721
+ /**
722
+ * Drop records that fall outside the run's scope root, grouping by JSON:API
723
+ * type so `ScopeGuard` can build one predicate per type. A no-op for an
724
+ * unscoped run — `ScopeGuard.filter` returns everything in that case, so the
725
+ * short-circuit only avoids a pointless round trip.
726
+ */
727
+ async filterByScope(records, ctx) {
728
+ if (!ctx.scopeId || !ctx.scopeType || records.length === 0)
729
+ return records;
730
+ const byType = new Map();
731
+ for (const record of records) {
732
+ const type = typeof record.type === "string" ? record.type : undefined;
733
+ const id = typeof record.id === "string" ? record.id : undefined;
734
+ if (!type || !id)
735
+ continue;
736
+ const bucket = byType.get(type) ?? [];
737
+ bucket.push(record);
738
+ byType.set(type, bucket);
739
+ }
740
+ const kept = new Set();
741
+ for (const [type, bucket] of byType) {
742
+ const allowed = await this.scopeGuard.filter({ type, records: bucket, ctx });
743
+ for (const record of allowed)
744
+ kept.add(record);
745
+ }
746
+ return records.filter((record) => kept.has(record));
747
+ }
450
748
  /**
451
749
  * Resolve the content scope from the assistant's BOUND_TO relationship if
452
- * loaded. Mirrors the inline logic in `runAgentTurn`.
750
+ * loaded.
453
751
  */
454
752
  async resolveContentScope(assistantId) {
455
753
  try {
@@ -473,7 +771,7 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
473
771
  assistantId: params.assistantId,
474
772
  turnStartedAt: now.toISOString().slice(0, 19).replace(/[T:]/g, "-"),
475
773
  });
476
- const hydrationContent = await this.buildHydrationMessage(params.priorMessages, params.userModuleIds);
774
+ const hydrationContent = await this.buildHydrationMessage(params.priorMessages, params.ctx, params.pinned);
477
775
  const trimmed = params.priorMessages.slice(-exports.MAX_MESSAGES_TO_LLM).map((m) => ({
478
776
  type: this.roleToType(m.role),
479
777
  content: m.content,
@@ -483,15 +781,25 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
483
781
  ...trimmed,
484
782
  { type: agentmessage_type_1.AgentMessageType.User, content: params.question },
485
783
  ];
784
+ const seedContexts = await this.collectSeedContexts({
785
+ ctx: params.ctx,
786
+ contentScope: params.contentScope,
787
+ question: params.question,
788
+ howToMode: params.howToMode,
789
+ limitToHowToId: params.limitToHowToId,
790
+ });
486
791
  return await this.operator.run({
487
- companyId: params.companyId,
488
- userId: params.userId,
489
- userModuleIds: params.userModuleIds,
792
+ companyId: params.ctx.companyId,
793
+ userId: params.ctx.userId,
794
+ userModuleIds: params.ctx.userModuleIds,
490
795
  contentId: params.contentScope.contentId,
491
796
  contentType: params.contentScope.contentType,
797
+ scopeId: params.ctx.scopeId,
798
+ scopeType: params.ctx.scopeType,
492
799
  messages,
493
800
  question: params.question,
494
801
  threadId: params.threadId,
802
+ seedContexts,
495
803
  });
496
804
  }
497
805
  /**
@@ -604,7 +912,7 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
604
912
  turnStartedAt: now.toISOString().slice(0, 19).replace(/[T:]/g, "-"),
605
913
  });
606
914
  }
607
- const hydrationContent = await this.buildHydrationMessage(params.priorMessages, params.userModuleIds);
915
+ const hydrationContent = await this.buildHydrationMessage(params.priorMessages, params.ctx, params.pinned);
608
916
  const trimmed = params.priorMessages.slice(-exports.MAX_MESSAGES_TO_LLM).map((m) => ({
609
917
  type: this.roleToType(m.role),
610
918
  content: m.content,
@@ -614,32 +922,28 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
614
922
  ...trimmed,
615
923
  { type: agentmessage_type_1.AgentMessageType.User, content: params.newUserMessage.content },
616
924
  ];
617
- // Resolve content scope from the assistant's BOUND_TO relationship if loaded.
618
- let contentId;
619
- let contentType;
620
- if (params.assistantId) {
621
- try {
622
- const assistant = await this.repository.findById({ id: params.assistantId });
623
- const c = assistant.content;
624
- contentId = c?.id;
625
- contentType = c?.type;
626
- }
627
- catch {
628
- // If load fails, proceed without content scope.
629
- }
630
- }
925
+ const seedContexts = await this.collectSeedContexts({
926
+ ctx: params.ctx,
927
+ contentScope: params.contentScope,
928
+ question: params.newUserMessage.content,
929
+ howToMode: params.howToMode,
930
+ limitToHowToId: params.limitToHowToId,
931
+ });
631
932
  const response = await this.responder.run({
632
- companyId: params.companyId,
633
- userId: params.userId,
634
- userModuleIds: params.userModuleIds,
635
- contentId,
636
- contentType,
933
+ companyId: params.ctx.companyId,
934
+ userId: params.ctx.userId,
935
+ userModuleIds: params.ctx.userModuleIds,
936
+ contentId: params.contentScope.contentId,
937
+ contentType: params.contentScope.contentType,
938
+ scopeId: params.ctx.scopeId,
939
+ scopeType: params.ctx.scopeType,
637
940
  dataLimits: {
638
941
  howToMode: params.howToMode,
639
942
  limitToHowToId: params.limitToHowToId,
640
943
  },
641
944
  messages,
642
945
  question: params.newUserMessage.content,
946
+ seedContexts,
643
947
  });
644
948
  return {
645
949
  id: (0, crypto_1.randomUUID)(),
@@ -673,14 +977,17 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
673
977
  * the whole builder returns null so a single bad Neo4j hiccup does not
674
978
  * fail the chat turn.
675
979
  */
676
- async buildHydrationMessage(messages, userModuleIds) {
677
- if (messages.length === 0)
980
+ async buildHydrationMessage(messages, ctx, pinned = []) {
981
+ if (messages.length === 0 && pinned.length === 0)
678
982
  return null;
679
983
  try {
680
- const pairs = await this.assistantMessageRepo.findReferencedTypeIdPairs({
681
- messageIds: messages.map((m) => m.id),
682
- });
683
- if (pairs.length === 0)
984
+ const userModuleIds = ctx.userModuleIds;
985
+ const pairs = messages.length
986
+ ? await this.assistantMessageRepo.findReferencedTypeIdPairs({
987
+ messageIds: messages.map((m) => m.id),
988
+ })
989
+ : [];
990
+ if (pairs.length === 0 && pinned.length === 0)
684
991
  return null;
685
992
  // The most recent assistant message is the focus anchor.
686
993
  const prevAssistantId = [...messages].reverse().find((m) => m.role === "assistant")?.id;
@@ -690,6 +997,16 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
690
997
  const backgroundKeys = new Set();
691
998
  const focusRefs = [];
692
999
  const backgroundRefs = [];
1000
+ // Entities the user named explicitly lead the focus tier: they are the
1001
+ // subject of the new question, so they must survive FOCUS_CAP and must
1002
+ // never be demoted to a background stub by an earlier mention.
1003
+ for (const mention of pinned) {
1004
+ const key = `${mention.type}/${mention.id}`;
1005
+ if (focusKeys.has(key))
1006
+ continue;
1007
+ focusKeys.add(key);
1008
+ focusRefs.push({ type: mention.type, id: mention.id });
1009
+ }
693
1010
  for (const p of pairs) {
694
1011
  const key = `${p.type}/${p.id}`;
695
1012
  if (prevAssistantId && p.messageId === prevAssistantId) {
@@ -716,9 +1033,12 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
716
1033
  if (backgroundOnly.length > BACKGROUND_CAP) {
717
1034
  this.assistantLogger.warn(`buildHydrationMessage: background set capped at ${BACKGROUND_CAP} (had ${backgroundOnly.length})`);
718
1035
  }
719
- // Load focus records (full) and background records (project to label).
720
- const focusRecords = await this.loadFocusRecords(focusCapped, userModuleIds);
721
- const backgroundStubs = await this.loadBackgroundStubs(backgroundCapped, userModuleIds);
1036
+ // Load focus records (full) and background records (project to label),
1037
+ // then drop anything outside the run's scope. Hydration is a data-access
1038
+ // point like any tool: a record from another campaign must never reach
1039
+ // the LLM just because an earlier turn referenced it.
1040
+ const focusRecords = await this.filterByScope(await this.loadFocusRecords(focusCapped, userModuleIds), ctx);
1041
+ const backgroundStubs = await this.filterByScope(await this.loadBackgroundStubs(backgroundCapped, userModuleIds), ctx);
722
1042
  // Diagnostic trace: in the dump it lets us see exactly which entities were
723
1043
  // handed to the LLM as focus context (and whether any were bridges, since
724
1044
  // bridge ids in focus are addressable directly without resolve_entity).
@@ -737,6 +1057,9 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
737
1057
  sections.push('These are the entities your previous answer was about. When the user\'s new question refers to any of them — by name or implicitly ("these", "them", "other orders", "their invoices") — use their id directly. Do not call resolve_entity for a name that matches one of these.');
738
1058
  sections.push("");
739
1059
  sections.push(JSON.stringify(focusRecords, null, 2));
1060
+ if (pinned.length > 0) {
1061
+ sections.push("The user named some of these entities explicitly in the new question. Treat every entity listed above as already identified: use its id directly and do not call resolve_entity for its name.");
1062
+ }
740
1063
  sections.push("");
741
1064
  }
742
1065
  if (backgroundStubs.length > 0) {
@@ -862,6 +1185,8 @@ let AssistantService = AssistantService_1 = class AssistantService extends abstr
862
1185
  exports.AssistantService = AssistantService;
863
1186
  exports.AssistantService = AssistantService = AssistantService_1 = __decorate([
864
1187
  (0, common_1.Injectable)(),
1188
+ __param(17, (0, common_1.Optional)()),
1189
+ __param(17, (0, common_1.Inject)(seed_context_interface_1.ASSISTANT_SEED_CONTEXT_PROVIDERS)),
865
1190
  __metadata("design:paramtypes", [jsonapi_service_1.JsonApiService,
866
1191
  assistant_repository_1.AssistantRepository,
867
1192
  nestjs_cls_1.ClsService,
@@ -875,6 +1200,9 @@ exports.AssistantService = AssistantService = AssistantService_1 = __decorate([
875
1200
  assistant_action_service_1.AssistantActionService,
876
1201
  assistant_action_repository_1.AssistantActionRepository,
877
1202
  websocket_service_1.WebSocketService,
878
- config_1.ConfigService])
1203
+ config_1.ConfigService,
1204
+ assistant_mention_extractor_1.AssistantMentionExtractor,
1205
+ blocknote_service_1.BlockNoteService,
1206
+ scope_guard_1.ScopeGuard, Array])
879
1207
  ], AssistantService);
880
1208
  //# sourceMappingURL=assistant.service.js.map