@indexnetwork/protocol 14.3.2-rc.477.1 → 16.1.1-rc.478.1

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 (140) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/IMPLEMENTATION.md +50 -52
  3. package/STABILITY.md +3 -3
  4. package/dist/chat/chat.agent.js +2 -2
  5. package/dist/chat/chat.graph.d.ts +41 -1
  6. package/dist/chat/chat.graph.js +108 -127
  7. package/dist/discovery/hyde.frame.d.ts +4 -4
  8. package/dist/discovery/hyde.graph.d.ts +71 -12
  9. package/dist/discovery/hyde.graph.js +337 -335
  10. package/dist/discovery/lens.inferrer.d.ts +6 -6
  11. package/dist/enrichment/enrichment.graph.d.ts +127 -9
  12. package/dist/enrichment/enrichment.graph.js +633 -655
  13. package/dist/enrichment/enrichment.state.d.ts +1 -1
  14. package/dist/enrichment/enrichment.tools.context-read.d.ts +10 -0
  15. package/dist/enrichment/enrichment.tools.context-read.js +353 -0
  16. package/dist/enrichment/enrichment.tools.context-write.d.ts +9 -0
  17. package/dist/enrichment/enrichment.tools.context-write.js +417 -0
  18. package/dist/enrichment/enrichment.tools.d.ts +11 -1
  19. package/dist/enrichment/enrichment.tools.helpers.d.ts +139 -0
  20. package/dist/enrichment/enrichment.tools.helpers.js +234 -0
  21. package/dist/enrichment/enrichment.tools.js +16 -975
  22. package/dist/index.d.ts +27 -48
  23. package/dist/index.js +17 -27
  24. package/dist/intents/application/intent.graph.d.ts +71 -67
  25. package/dist/intents/application/intent.graph.execute.d.ts +59 -0
  26. package/dist/intents/application/intent.graph.execute.js +301 -0
  27. package/dist/intents/application/intent.graph.infer.d.ts +44 -0
  28. package/dist/intents/application/intent.graph.infer.js +97 -0
  29. package/dist/intents/application/intent.graph.js +96 -888
  30. package/dist/intents/application/intent.graph.reconcile.d.ts +71 -0
  31. package/dist/intents/application/intent.graph.reconcile.js +274 -0
  32. package/dist/intents/application/intent.graph.shared.d.ts +70 -0
  33. package/dist/intents/application/intent.graph.shared.js +153 -0
  34. package/dist/intents/domain/intent.state.d.ts +1 -1
  35. package/dist/maintenance/maintenance.graph.d.ts +66 -3
  36. package/dist/maintenance/maintenance.graph.js +155 -156
  37. package/dist/mcp/mcp.authorization-policy.d.ts +24 -28
  38. package/dist/mcp/mcp.authorization-policy.js +14 -34
  39. package/dist/mcp/mcp.server.d.ts +3 -3
  40. package/dist/mcp/mcp.server.js +12 -16
  41. package/dist/negotiations/application/negotiation.candidates.d.ts +83 -0
  42. package/dist/negotiations/application/negotiation.candidates.js +162 -0
  43. package/dist/negotiations/application/negotiation.graph.d.ts +91 -157
  44. package/dist/negotiations/application/negotiation.graph.finalize.d.ts +5 -0
  45. package/dist/negotiations/application/negotiation.graph.finalize.js +280 -0
  46. package/dist/negotiations/application/negotiation.graph.init.d.ts +54 -0
  47. package/dist/negotiations/application/negotiation.graph.init.js +227 -0
  48. package/dist/negotiations/application/negotiation.graph.js +69 -1388
  49. package/dist/negotiations/application/negotiation.graph.screen.d.ts +23 -0
  50. package/dist/negotiations/application/negotiation.graph.screen.js +108 -0
  51. package/dist/negotiations/application/negotiation.graph.shared.d.ts +75 -0
  52. package/dist/negotiations/application/negotiation.graph.shared.js +125 -0
  53. package/dist/negotiations/application/negotiation.graph.turn.d.ts +105 -0
  54. package/dist/negotiations/application/negotiation.graph.turn.js +484 -0
  55. package/dist/negotiations/domain/negotiation.state.d.ts +1 -1
  56. package/dist/negotiations/domain/negotiation.state.js +0 -1
  57. package/dist/networks/application/indexer.graph.d.ts +165 -7
  58. package/dist/networks/application/indexer.graph.js +339 -388
  59. package/dist/networks/application/indexer.state.d.ts +1 -1
  60. package/dist/networks/application/membership.graph.d.ts +83 -5
  61. package/dist/networks/application/membership.graph.js +177 -207
  62. package/dist/networks/application/network.graph.d.ts +148 -5
  63. package/dist/networks/application/network.graph.js +249 -278
  64. package/dist/networks/domain/membership.state.d.ts +1 -1
  65. package/dist/networks/domain/network.state.d.ts +1 -1
  66. package/dist/opportunities/application/delivery-card.cache.d.ts +1 -1
  67. package/dist/opportunities/application/delivery-card.cache.js +2 -2
  68. package/dist/opportunities/application/index.d.ts +2 -2
  69. package/dist/opportunities/application/index.js +2 -2
  70. package/dist/opportunities/application/opportunity.evaluator.js +1 -1
  71. package/dist/opportunities/application/opportunity.graph.d.ts +686 -473
  72. package/dist/opportunities/application/opportunity.graph.discovery-strategies.d.ts +109 -0
  73. package/dist/opportunities/application/opportunity.graph.discovery-strategies.js +451 -0
  74. package/dist/opportunities/application/opportunity.graph.discovery.d.ts +69 -0
  75. package/dist/opportunities/application/opportunity.graph.discovery.js +397 -0
  76. package/dist/opportunities/application/opportunity.graph.evaluation.d.ts +82 -0
  77. package/dist/opportunities/application/opportunity.graph.evaluation.js +608 -0
  78. package/dist/opportunities/application/opportunity.graph.js +120 -3690
  79. package/dist/opportunities/application/opportunity.graph.modes.d.ts +538 -0
  80. package/dist/opportunities/application/opportunity.graph.modes.js +538 -0
  81. package/dist/opportunities/application/opportunity.graph.negotiate.d.ts +109 -0
  82. package/dist/opportunities/application/opportunity.graph.negotiate.js +392 -0
  83. package/dist/opportunities/application/opportunity.graph.persist-node.d.ts +93 -0
  84. package/dist/opportunities/application/opportunity.graph.persist-node.js +765 -0
  85. package/dist/opportunities/application/opportunity.graph.prep.d.ts +150 -0
  86. package/dist/opportunities/application/opportunity.graph.prep.js +381 -0
  87. package/dist/opportunities/application/opportunity.graph.shared.d.ts +173 -0
  88. package/dist/opportunities/application/opportunity.graph.shared.js +199 -0
  89. package/dist/opportunities/application/opportunity.presentation.d.ts +399 -0
  90. package/dist/opportunities/application/{opportunity.presenter.js → opportunity.presentation.js} +735 -9
  91. package/dist/opportunities/application/opportunity.tools.cards.d.ts +152 -0
  92. package/dist/opportunities/application/opportunity.tools.cards.js +235 -0
  93. package/dist/opportunities/application/opportunity.tools.d.ts +8 -114
  94. package/dist/opportunities/application/opportunity.tools.js +28 -711
  95. package/dist/opportunities/application/opportunity.tools.list.d.ts +10 -0
  96. package/dist/opportunities/application/opportunity.tools.list.js +493 -0
  97. package/dist/opportunities/domain/index.d.ts +3 -3
  98. package/dist/opportunities/domain/index.js +3 -3
  99. package/dist/opportunities/domain/opportunity.state.d.ts +19 -19
  100. package/dist/opportunities/index.d.ts +6 -6
  101. package/dist/opportunities/index.js +4 -4
  102. package/dist/opportunities/ports/opportunity.tools.port.d.ts +1 -1
  103. package/dist/opportunities/radar/radar.graph.d.ts +57 -13
  104. package/dist/opportunities/radar/radar.graph.js +470 -471
  105. package/dist/premises/premise.graph.d.ts +88 -20
  106. package/dist/premises/premise.graph.js +207 -218
  107. package/dist/questions/domain/question.schema.d.ts +40 -40
  108. package/dist/shared/agent/tool.helpers.d.ts +21 -0
  109. package/dist/shared/agent/tool.registry.d.ts +4 -3
  110. package/dist/shared/agent/tool.registry.js +1 -1
  111. package/dist/shared/interfaces/database.capabilities.d.ts +100 -0
  112. package/dist/shared/interfaces/database.capabilities.js +7 -0
  113. package/dist/shared/interfaces/database.entities.d.ts +533 -0
  114. package/dist/shared/interfaces/database.entities.js +10 -0
  115. package/dist/shared/interfaces/database.identity-queries.d.ts +294 -0
  116. package/dist/shared/interfaces/database.identity-queries.js +4 -0
  117. package/dist/shared/interfaces/database.interface.d.ts +15 -2277
  118. package/dist/shared/interfaces/database.interface.js +8 -0
  119. package/dist/shared/interfaces/database.member-queries.d.ts +228 -0
  120. package/dist/shared/interfaces/database.member-queries.js +4 -0
  121. package/dist/shared/interfaces/database.negotiation.d.ts +305 -0
  122. package/dist/shared/interfaces/database.negotiation.js +26 -0
  123. package/dist/shared/interfaces/database.network-queries.d.ts +277 -0
  124. package/dist/shared/interfaces/database.network-queries.js +4 -0
  125. package/dist/shared/interfaces/database.opportunity-queries.d.ts +285 -0
  126. package/dist/shared/interfaces/database.opportunity-queries.js +4 -0
  127. package/dist/shared/interfaces/database.port.d.ts +322 -0
  128. package/dist/shared/interfaces/database.port.js +29 -0
  129. package/dist/shared/schemas/discovery-question.schema.d.ts +14 -14
  130. package/dist/shared/schemas/negotiation-digest.schema.d.ts +2 -2
  131. package/package.json +2 -15
  132. package/dist/opportunities/application/opportunity.card-presentation.d.ts +0 -44
  133. package/dist/opportunities/application/opportunity.card-presentation.js +0 -93
  134. package/dist/opportunities/application/opportunity.presenter.d.ts +0 -156
  135. package/dist/opportunities/domain/opportunity.presentation-cache.d.ts +0 -5
  136. package/dist/opportunities/domain/opportunity.presentation-cache.js +0 -11
  137. package/dist/opportunities/domain/opportunity.presentation.d.ts +0 -76
  138. package/dist/opportunities/domain/opportunity.presentation.js +0 -516
  139. package/dist/opportunities/domain/opportunity.safe-presentation.d.ts +0 -104
  140. package/dist/opportunities/domain/opportunity.safe-presentation.js +0 -102
@@ -0,0 +1,10 @@
1
+ /**
2
+ * `list_opportunities` — the persisted opportunity feed for chat and MCP.
3
+ *
4
+ * Split out of `opportunity.tools.ts`: the listing is by far the largest of the
5
+ * three opportunity tools, and it is the only one that renders cards.
6
+ */
7
+ import type { DefineTool } from "../../shared/agent/tool.helpers.js";
8
+ import type { OpportunityToolDeps } from "../ports/index.js";
9
+ /** Builds the `list_opportunities` tool against the host's capabilities. */
10
+ export declare function createListOpportunitiesTool(defineTool: DefineTool, deps: OpportunityToolDeps): any;
@@ -0,0 +1,493 @@
1
+ /**
2
+ * `list_opportunities` — the persisted opportunity feed for chat and MCP.
3
+ *
4
+ * Split out of `opportunity.tools.ts`: the listing is by far the largest of the
5
+ * three opportunity tools, and it is the only one that renders cards.
6
+ */
7
+ import { z } from "zod";
8
+ import { success, error, UUID_REGEX } from "../../shared/agent/tool.helpers.js";
9
+ import { focusedIntentId, focusedNetworkId, focusedNetworkLabel } from "../../shared/agent/tool.scope.js";
10
+ import { getPrimaryActionLabel, SECONDARY_ACTION_LABEL } from "../domain/opportunity.labels.js";
11
+ import { OpportunityPresenter, gatherPresenterContext, stripUuids } from "./opportunity.presentation.js";
12
+ import { buildOpportunityPresentation } from "./opportunity.presentation.js";
13
+ import { loadNegotiationContext } from "./negotiation-context.loader.js";
14
+ import { selectOpportunityFeed } from './opportunity.feed-selection.js';
15
+ import { CHAT_DISPLAY_LIMIT, attachOpportunityAppLink, attachProfileLink, buildNegotiationUrl } from "./opportunity.tools.cards.js";
16
+ import { logger, stripLeadingNarratorName } from "./opportunity.tools.cards.js";
17
+ /** Builds the `list_opportunities` tool against the host's capabilities. */
18
+ export function createListOpportunitiesTool(defineTool, deps) {
19
+ const { database, userDb, systemDb, graphs, cache } = deps;
20
+ const createOpportunityPresenter = deps.opportunityPresentation?.createPresenter ??
21
+ (() => new OpportunityPresenter());
22
+ const gatherOpportunityPresenterContext = deps.opportunityPresentation?.gatherPresenterContext ??
23
+ gatherPresenterContext;
24
+ const listOpportunities = defineTool({
25
+ name: "list_opportunities",
26
+ description: "Lists the authenticated user's actionable opportunities (discovered connections). Returns opportunity cards ready for display.\n\n" +
27
+ "**What are opportunities?** Matches between users whose intents complement each other within shared networkes. " +
28
+ "Each opportunity has a status: draft (not yet sent), pending (sent, awaiting response), accepted, rejected, or expired.\n\n" +
29
+ "**What this returns:** Only draft and pending opportunities — the ones the user can still act on. " +
30
+ "Accepted, rejected, and expired ones are not surfaced through this tool.\n\n" +
31
+ "**When to use:** When the user wants to see their current matches or review what's waiting for their response.\n\n" +
32
+ "**Returns:** Up to 3 opportunity code blocks (interactive cards) with counterpart name, match reasoning, confidence score, " +
33
+ "and current status. Use update_opportunity to act on them (send, accept, reject).",
34
+ querySchema: z.object({
35
+ networkId: z
36
+ .string()
37
+ .optional()
38
+ .describe("Network UUID to filter opportunities to a specific community. Get from read_networks. Defaults to the scoped network in network-scoped chats. Omit to see opportunities across all networks."),
39
+ scopeType: z
40
+ .enum(['intent'])
41
+ .optional()
42
+ .describe("Optional selected scope type. Use 'intent' to narrow listed opportunities to a selected intent."),
43
+ scopeId: z
44
+ .string()
45
+ .optional()
46
+ .describe("Selected intent UUID when scopeType is 'intent'. Ignored only when absent."),
47
+ includeDigestMarkers: z
48
+ .boolean()
49
+ .optional()
50
+ .describe("Internal scheduled-digest mode only. When true, includes hidden delivery markers so the digest send pass can confirm only edited-in opportunities."),
51
+ }),
52
+ handler: async ({ context, query }) => {
53
+ const scopedNetworkId = focusedNetworkId(context) ?? context.networkId?.trim();
54
+ const scopedIndexLabel = focusedNetworkLabel(context);
55
+ // Strict scope enforcement: when chat is network-scoped, only allow that index
56
+ if (scopedNetworkId &&
57
+ query.networkId?.trim() &&
58
+ query.networkId.trim() !== scopedNetworkId) {
59
+ return error(`This chat is scoped to ${scopedIndexLabel}. You can only list opportunities from this community.`);
60
+ }
61
+ const effectiveIndexId = (scopedNetworkId || query.networkId?.trim()) ?? undefined;
62
+ if (effectiveIndexId && !UUID_REGEX.test(effectiveIndexId)) {
63
+ return error("Invalid network ID format.");
64
+ }
65
+ const contextIntentId = focusedIntentId(context);
66
+ const rawScopeId = query.scopeId?.trim() || undefined;
67
+ if (query.scopeType === 'intent' && !rawScopeId) {
68
+ return error("scopeId required when scopeType is intent.");
69
+ }
70
+ if (!query.scopeType && rawScopeId) {
71
+ return error("scopeType=intent required when scopeId is provided.");
72
+ }
73
+ if (rawScopeId && !UUID_REGEX.test(rawScopeId)) {
74
+ return error("Invalid scope ID format.");
75
+ }
76
+ if (contextIntentId && rawScopeId && contextIntentId !== rawScopeId) {
77
+ return error("This chat is scoped to a different intent.");
78
+ }
79
+ const effectiveIntentScope = contextIntentId
80
+ ? { scopeType: 'intent', scopeId: contextIntentId }
81
+ : query.scopeType === 'intent' && rawScopeId
82
+ ? { scopeType: 'intent', scopeId: rawScopeId }
83
+ : {};
84
+ const selection = await selectOpportunityFeed({
85
+ reader: database,
86
+ deliveryLedger: deps.deliveryLedger,
87
+ viewerId: context.userId,
88
+ networkId: effectiveIndexId,
89
+ intentScope: effectiveIntentScope,
90
+ isMcp: context.isMcp === true,
91
+ includeDigestMarkers: query.includeDigestMarkers,
92
+ displayLimit: CHAT_DISPLAY_LIMIT,
93
+ warn: (message, data) => logger.warn(message, data),
94
+ });
95
+ const { opportunities, dedupedCount, skippedIds, redeliveryIds, fetchedCount, isDigestMode } = selection;
96
+ const buildListDebugSteps = () => {
97
+ const steps = [];
98
+ if (skippedIds.length > 0) {
99
+ steps.push({
100
+ step: "opportunity_display_skips",
101
+ detail: `${skippedIds.length} opportunity card(s) couldn't be displayed`,
102
+ data: {
103
+ skippedCount: skippedIds.length,
104
+ totalOpportunities: fetchedCount,
105
+ skippedOpportunityIds: skippedIds,
106
+ },
107
+ });
108
+ }
109
+ return steps;
110
+ };
111
+ if (!opportunities || opportunities.length === 0) {
112
+ if (skippedIds.length > 0) {
113
+ const listDebugSteps = buildListDebugSteps();
114
+ return success({
115
+ found: false,
116
+ count: 0,
117
+ summary: "Some opportunities couldn't be displayed",
118
+ message: "I found opportunities, but couldn't render them. Please try again.",
119
+ ...(listDebugSteps.length ? { debugSteps: listDebugSteps } : {}),
120
+ });
121
+ }
122
+ // Digest mode: distinguish "everything was already shown" from "nothing
123
+ // exists" so the brief omits the people section instead of prompting
124
+ // the user to run discovery.
125
+ if (isDigestMode && dedupedCount > 0) {
126
+ return success({
127
+ found: false,
128
+ count: 0,
129
+ summary: "No new opportunities to show",
130
+ message: "No new opportunities today — everything actionable has already been shown recently. Omit the people section from the digest.",
131
+ });
132
+ }
133
+ return success({
134
+ found: false,
135
+ count: 0,
136
+ summary: "No opportunities yet",
137
+ message: "You have no opportunities yet. Create or refine an approved signal with create_intent or update_intent; matches are created in the background. Use list_opportunities later to review persisted results.",
138
+ });
139
+ }
140
+ // Batch-fetch profiles and users for all counterpart and introducer userIds to avoid N+1
141
+ const counterpartUserIds = new Set();
142
+ const introducerUserIds = new Set();
143
+ for (const opp of opportunities) {
144
+ const counterpartActor = opp.actors.find((a) => a.userId !== context.userId && a.role !== "introducer");
145
+ if (counterpartActor?.userId)
146
+ counterpartUserIds.add(counterpartActor.userId);
147
+ const introducerActor = opp.actors.find((a) => a.role === "introducer" && a.userId !== context.userId);
148
+ if (introducerActor?.userId)
149
+ introducerUserIds.add(introducerActor.userId);
150
+ }
151
+ const allUserIds = [
152
+ ...new Set([...counterpartUserIds, ...introducerUserIds]),
153
+ ];
154
+ const [profileResults, userResults] = await Promise.all([
155
+ Promise.all(allUserIds.map((id) => database.getProfile(id))),
156
+ Promise.all(allUserIds.map((id) => database.getUser(id))),
157
+ ]);
158
+ const profileMap = new Map();
159
+ const userMap = new Map();
160
+ allUserIds.forEach((userId, i) => {
161
+ const profile = profileResults[i] ?? null;
162
+ const user = userResults[i] ?? null;
163
+ if (profile)
164
+ profileMap.set(userId, profile);
165
+ if (user)
166
+ userMap.set(userId, user);
167
+ });
168
+ const cardDataList = [];
169
+ const seenOpportunityIds = new Set();
170
+ if (isDigestMode) {
171
+ // ── Digest mode: use LLM presenter for rich, second-person card text ──
172
+ const presenter = createOpportunityPresenter();
173
+ const presenterDb = database;
174
+ const PRESENTER_CONCURRENCY = 6;
175
+ for (let i = 0; i < opportunities.length; i += PRESENTER_CONCURRENCY) {
176
+ const chunk = opportunities.slice(i, i + PRESENTER_CONCURRENCY);
177
+ const chunkCards = await Promise.all(chunk.map(async (opp) => {
178
+ if (seenOpportunityIds.has(opp.id))
179
+ return null;
180
+ seenOpportunityIds.add(opp.id);
181
+ try {
182
+ const counterpartActor = opp.actors.find((a) => a.userId !== context.userId && a.role !== "introducer");
183
+ const counterpartUserId = counterpartActor?.userId;
184
+ if (!counterpartUserId)
185
+ return null;
186
+ const viewerIsIntroducerHere = opp.actors.some((a) => a.role === "introducer" && a.userId === context.userId);
187
+ const secondPartyActorForHeadline = viewerIsIntroducerHere
188
+ ? opp.actors.find((a) => a.userId !== context.userId &&
189
+ a.userId !== counterpartUserId &&
190
+ a.role !== "introducer")
191
+ : undefined;
192
+ const introducerActor = opp.actors.find((a) => a.role === "introducer" && a.userId !== context.userId);
193
+ const createdByName = opp.detection.createdByName;
194
+ const counterpartUser = userMap.get(counterpartUserId) ?? null;
195
+ const counterpartName = profileMap.get(counterpartUserId)?.identity?.name ??
196
+ counterpartUser?.name ??
197
+ "Someone";
198
+ const introducerName = createdByName ??
199
+ (introducerActor
200
+ ? (profileMap.get(introducerActor.userId)?.identity?.name ?? null)
201
+ : null);
202
+ const introducerUser = introducerActor
203
+ ? userMap.get(introducerActor.userId) ?? null
204
+ : null;
205
+ const secondPartyUser = secondPartyActorForHeadline
206
+ ? userMap.get(secondPartyActorForHeadline.userId) ?? null
207
+ : null;
208
+ const secondPartyNameForHeadline = secondPartyActorForHeadline
209
+ ? (profileMap.get(secondPartyActorForHeadline.userId)?.identity?.name ??
210
+ secondPartyUser?.name ??
211
+ undefined)
212
+ : undefined;
213
+ const viewerActor = opp.actors.find((a) => a.userId === context.userId);
214
+ const viewerRole = viewerActor?.role ?? "party";
215
+ const isCounterpartGhost = counterpartUser?.isGhost ?? false;
216
+ try {
217
+ // Load the negotiation context alongside presenter context so
218
+ // the digest copy can explain *why* the opportunity surfaced
219
+ // (EDG-50) — the presenter grounds `digestSummary` in concrete
220
+ // negotiation turns when this is present. The same context
221
+ // yields the conversationId for the negotiation-trace link
222
+ // (EDG-51).
223
+ const [ctx, negotiationContext] = await Promise.all([
224
+ gatherOpportunityPresenterContext(presenterDb, opp, context.userId, counterpartUserId),
225
+ loadNegotiationContext(deps.negotiationDatabase, opp.id, opp.status),
226
+ ]);
227
+ const presentation = await presenter.presentCard({
228
+ ...ctx,
229
+ opportunityStatus: opp.status,
230
+ ...(negotiationContext ? { negotiationContext } : {}),
231
+ });
232
+ const negotiationUrl = buildNegotiationUrl(negotiationContext?.conversationId, deps.frontendUrl);
233
+ // Build narrator chip from presenter output
234
+ let narratorChip;
235
+ const introducerIsCounterpart = introducerActor && counterpartActor && introducerActor.userId === counterpartActor.userId;
236
+ if (introducerActor && introducerActor.userId !== context.userId && !introducerIsCounterpart) {
237
+ const narratorName = introducerName?.trim() || "Someone";
238
+ narratorChip = {
239
+ name: narratorName,
240
+ text: stripLeadingNarratorName(presentation.narratorRemark, narratorName),
241
+ avatar: introducerUser?.avatar ?? null,
242
+ userId: introducerActor.userId,
243
+ };
244
+ }
245
+ else if (introducerActor?.userId === context.userId) {
246
+ narratorChip = { name: "You", text: presentation.narratorRemark, userId: context.userId };
247
+ }
248
+ else {
249
+ narratorChip = { name: "Index", text: presentation.narratorRemark };
250
+ }
251
+ const card = {
252
+ opportunityId: opp.id,
253
+ userId: counterpartUserId,
254
+ name: counterpartName,
255
+ avatar: counterpartUser?.avatar ?? null,
256
+ mainText: stripUuids(presentation.personalizedSummary),
257
+ digestSummary: stripUuids(presentation.digestSummary),
258
+ // Deep-link to the negotiation trace that produced this card
259
+ // (EDG-51). Only present when a negotiation conversation exists.
260
+ ...(negotiationUrl ? { negotiationUrl } : {}),
261
+ cta: presentation.suggestedAction,
262
+ headline: viewerIsIntroducerHere && secondPartyNameForHeadline
263
+ ? `${counterpartName} → ${secondPartyNameForHeadline}`
264
+ : presentation.headline,
265
+ primaryActionLabel: getPrimaryActionLabel(viewerRole),
266
+ secondaryActionLabel: SECONDARY_ACTION_LABEL,
267
+ mutualIntentsLabel: presentation.mutualIntentsLabel,
268
+ narratorChip,
269
+ viewerRole,
270
+ score: typeof opp.interpretation?.confidence === "number"
271
+ ? opp.interpretation.confidence
272
+ : undefined,
273
+ status: opp.status,
274
+ isGhost: isCounterpartGhost,
275
+ ...(redeliveryIds.has(opp.id) ? { redelivery: true } : {}),
276
+ ...(viewerIsIntroducerHere && secondPartyNameForHeadline
277
+ ? {
278
+ secondParty: {
279
+ name: secondPartyNameForHeadline,
280
+ ...(secondPartyUser?.avatar != null ? { avatar: secondPartyUser.avatar } : {}),
281
+ ...(secondPartyActorForHeadline?.userId ? { userId: secondPartyActorForHeadline.userId } : {}),
282
+ },
283
+ }
284
+ : {}),
285
+ };
286
+ // Attach the agent-facing profile and opportunity links for
287
+ // MCP callers
288
+ if (context.isMcp) {
289
+ attachProfileLink(card, {
290
+ counterpartUserId,
291
+ frontendUrl: deps.frontendUrl,
292
+ });
293
+ attachOpportunityAppLink(card, {
294
+ frontendUrl: deps.frontendUrl,
295
+ });
296
+ }
297
+ return card;
298
+ }
299
+ catch (presenterErr) {
300
+ logger.warn("LLM presenter failed for list_opportunities digest card, skipping raw fallback", {
301
+ opportunityId: opp.id,
302
+ err: presenterErr,
303
+ });
304
+ // Scheduled digests should only surface OpportunityPresenter-rendered
305
+ // copy. The minimal fallback reuses evaluator reasoning, which can
306
+ // contain raw narrator phrasing (for example "The discoverer...")
307
+ // and is not suitable for AgentVillage morning briefs.
308
+ skippedIds.push(opp.id);
309
+ return null;
310
+ }
311
+ }
312
+ catch (err) {
313
+ logger.warn("Skipping opportunity that failed to build card", {
314
+ opportunityId: opp.id,
315
+ err,
316
+ });
317
+ skippedIds.push(opp.id);
318
+ return null;
319
+ }
320
+ }));
321
+ for (const card of chunkCards) {
322
+ if (card)
323
+ cardDataList.push(card);
324
+ }
325
+ }
326
+ }
327
+ else {
328
+ // ── Chat/list mode: use OpportunityPresenter for user-facing card copy ──
329
+ const presenter = createOpportunityPresenter();
330
+ const presenterDb = database;
331
+ const PRESENTER_CONCURRENCY = 6;
332
+ for (let i = 0; i < opportunities.length; i += PRESENTER_CONCURRENCY) {
333
+ const chunk = opportunities.slice(i, i + PRESENTER_CONCURRENCY);
334
+ const chunkCards = await Promise.all(chunk.map(async (opp) => {
335
+ if (seenOpportunityIds.has(opp.id))
336
+ return null;
337
+ seenOpportunityIds.add(opp.id);
338
+ try {
339
+ const counterpartActor = opp.actors.find((a) => a.userId !== context.userId && a.role !== "introducer");
340
+ const counterpartUserId = counterpartActor?.userId;
341
+ if (!counterpartUserId)
342
+ return null;
343
+ const viewerIsIntroducerHere = opp.actors.some((a) => a.role === "introducer" && a.userId === context.userId);
344
+ const secondPartyActorForHeadline = viewerIsIntroducerHere
345
+ ? opp.actors.find((a) => a.userId !== context.userId &&
346
+ a.userId !== counterpartUserId &&
347
+ a.role !== "introducer")
348
+ : undefined;
349
+ const introducerActor = opp.actors.find((a) => a.role === "introducer" && a.userId !== context.userId);
350
+ const createdByName = opp.detection.createdByName;
351
+ const counterpartProfile = profileMap.get(counterpartUserId) ?? null;
352
+ const counterpartUser = userMap.get(counterpartUserId) ?? null;
353
+ const introducerProfile = introducerActor && !createdByName
354
+ ? profileMap.get(introducerActor.userId) ?? null
355
+ : null;
356
+ const counterpartName = counterpartProfile?.identity?.name ??
357
+ counterpartUser?.name ??
358
+ "Someone";
359
+ const introducerName = createdByName ??
360
+ (introducerActor ? introducerProfile?.identity?.name ?? null : null);
361
+ const introducerUser = introducerActor
362
+ ? userMap.get(introducerActor.userId) ?? null
363
+ : null;
364
+ const secondPartyUser = secondPartyActorForHeadline
365
+ ? userMap.get(secondPartyActorForHeadline.userId) ?? null
366
+ : null;
367
+ const secondPartyNameForHeadline = secondPartyActorForHeadline
368
+ ? (profileMap.get(secondPartyActorForHeadline.userId)?.identity?.name ??
369
+ secondPartyUser?.name ??
370
+ undefined)
371
+ : undefined;
372
+ const viewerActor = opp.actors.find((a) => a.userId === context.userId);
373
+ const viewerRole = viewerActor?.role ?? "party";
374
+ const isCounterpartGhost = counterpartUser?.isGhost ?? false;
375
+ const [ctx, negotiationContext] = await Promise.all([
376
+ gatherOpportunityPresenterContext(presenterDb, opp, context.userId, counterpartUserId),
377
+ loadNegotiationContext(deps.negotiationDatabase, opp.id, opp.status),
378
+ ]);
379
+ const presentation = await presenter.presentCard({
380
+ ...ctx,
381
+ opportunityStatus: opp.status,
382
+ ...(negotiationContext ? { negotiationContext } : {}),
383
+ });
384
+ let narratorChip;
385
+ const introducerIsCounterpart = introducerActor && counterpartActor && introducerActor.userId === counterpartActor.userId;
386
+ if (introducerActor && introducerActor.userId !== context.userId && !introducerIsCounterpart) {
387
+ const narratorName = introducerName?.trim() || "Someone";
388
+ narratorChip = {
389
+ name: narratorName,
390
+ text: stripLeadingNarratorName(presentation.narratorRemark, narratorName),
391
+ avatar: introducerUser?.avatar ?? null,
392
+ userId: introducerActor.userId,
393
+ };
394
+ }
395
+ else if (introducerActor?.userId === context.userId) {
396
+ narratorChip = { name: "You", text: presentation.narratorRemark, userId: context.userId };
397
+ }
398
+ else {
399
+ narratorChip = { name: "Index", text: presentation.narratorRemark };
400
+ }
401
+ const cardData = {
402
+ opportunityId: opp.id,
403
+ userId: counterpartUserId,
404
+ name: counterpartName,
405
+ avatar: counterpartUser?.avatar ?? null,
406
+ mainText: stripUuids(presentation.personalizedSummary),
407
+ cta: presentation.suggestedAction,
408
+ headline: viewerIsIntroducerHere && secondPartyNameForHeadline
409
+ ? `${counterpartName} → ${secondPartyNameForHeadline}`
410
+ : presentation.headline,
411
+ primaryActionLabel: getPrimaryActionLabel(viewerRole),
412
+ secondaryActionLabel: SECONDARY_ACTION_LABEL,
413
+ mutualIntentsLabel: presentation.mutualIntentsLabel,
414
+ narratorChip,
415
+ viewerRole,
416
+ score: typeof opp.interpretation?.confidence === "number"
417
+ ? opp.interpretation.confidence
418
+ : undefined,
419
+ status: opp.status,
420
+ isGhost: isCounterpartGhost,
421
+ ...(viewerIsIntroducerHere && secondPartyNameForHeadline
422
+ ? {
423
+ secondParty: {
424
+ name: secondPartyNameForHeadline,
425
+ ...(secondPartyUser?.avatar != null ? { avatar: secondPartyUser.avatar } : {}),
426
+ ...(secondPartyActorForHeadline?.userId ? { userId: secondPartyActorForHeadline.userId } : {}),
427
+ },
428
+ }
429
+ : {}),
430
+ };
431
+ // For MCP callers, attach the agent-facing profile link and the
432
+ // opportunity deep link so the agent never has to fabricate
433
+ // either. Accepting still happens in the Index app behind an
434
+ // authenticated call — the deep link only opens the card.
435
+ if (context.isMcp) {
436
+ attachProfileLink(cardData, {
437
+ counterpartUserId,
438
+ frontendUrl: deps.frontendUrl,
439
+ });
440
+ attachOpportunityAppLink(cardData, {
441
+ frontendUrl: deps.frontendUrl,
442
+ });
443
+ }
444
+ return cardData;
445
+ }
446
+ catch (err) {
447
+ logger.warn("Skipping opportunity that failed to build presenter card", {
448
+ opportunityId: opp.id,
449
+ err,
450
+ });
451
+ skippedIds.push(opp.id);
452
+ return null;
453
+ }
454
+ }));
455
+ for (const card of chunkCards) {
456
+ if (card)
457
+ cardDataList.push(card);
458
+ }
459
+ }
460
+ }
461
+ const listDebugSteps = buildListDebugSteps();
462
+ if (cardDataList.length === 0) {
463
+ if (skippedIds.length > 0) {
464
+ return success({
465
+ found: false,
466
+ count: 0,
467
+ summary: "Some opportunities couldn't be displayed",
468
+ message: "I found opportunities, but couldn't render them. Please try again.",
469
+ ...(listDebugSteps.length ? { debugSteps: listDebugSteps } : {}),
470
+ });
471
+ }
472
+ return success({
473
+ found: false,
474
+ count: 0,
475
+ summary: "No opportunities yet",
476
+ message: "You have no opportunities yet. Create or refine an approved signal with create_intent or update_intent; matches are created in the background. Use list_opportunities later to review persisted results.",
477
+ });
478
+ }
479
+ return success({
480
+ found: true,
481
+ count: cardDataList.length,
482
+ summary: `You have ${cardDataList.length} opportunity(ies)`,
483
+ message: buildOpportunityPresentation(cardDataList, {
484
+ isMcp: context.isMcp ?? false,
485
+ leadIn: `You have ${cardDataList.length} opportunity(ies).`,
486
+ includeDigestMarkers: context.isMcp === true && query.includeDigestMarkers === true,
487
+ }),
488
+ ...(listDebugSteps.length ? { debugSteps: listDebugSteps } : {}),
489
+ });
490
+ },
491
+ });
492
+ return listOpportunities;
493
+ }
@@ -29,9 +29,9 @@ export * from "./opportunity.utils.js";
29
29
  export * from "./opportunity.actor.js";
30
30
  export * from "./opportunity.evidence.js";
31
31
  export * from "../../shared/utils/claim-safety.js";
32
- export * from "./opportunity.presentation.js";
33
- export * from "./opportunity.safe-presentation.js";
34
- export * from "./opportunity.presentation-cache.js";
32
+ export * from "../application/opportunity.presentation.js";
33
+ export * from "../application/opportunity.presentation.js";
34
+ export * from "../application/opportunity.presentation.js";
35
35
  export * from "./negotiation-summary.builder.js";
36
36
  export type { PoolCandidate, DiscriminatorMiningInput, MinedDiscriminator, ScoredDiscriminator, VerifiedAssignment, DiscriminatorShadowResult, } from "../discriminator/discriminator.types.js";
37
37
  export { poolQuestionsMiningMode, poolQuestionsMode, poolQuestionsPushMode, poolQuestionsStampNewborn, POOL_DISCRIMINATOR_MIN_POOL_SIZE, POOL_DISCRIMINATOR_MAX_CANDIDATES, POOL_DISCRIMINATOR_MAX_PUBLIC_CONTEXT_CHARS, POOL_QUESTION_MIN_VOI, POOL_QUESTION_MAX_PENDING_PER_INTENT, poolQuestionsRanking, POOL_RERUN_DEBOUNCE_MS, poolQuestionsVisitTrigger, POOL_VISIT_MINING_DEBOUNCE_MS, } from "../discriminator/discriminator.env.js";
@@ -30,9 +30,9 @@ export * from "./opportunity.utils.js";
30
30
  export * from "./opportunity.actor.js";
31
31
  export * from "./opportunity.evidence.js";
32
32
  export * from "../../shared/utils/claim-safety.js";
33
- export * from "./opportunity.presentation.js";
34
- export * from "./opportunity.safe-presentation.js";
35
- export * from "./opportunity.presentation-cache.js";
33
+ export * from "../application/opportunity.presentation.js";
34
+ export * from "../application/opportunity.presentation.js";
35
+ export * from "../application/opportunity.presentation.js";
36
36
  export * from "./negotiation-summary.builder.js";
37
37
  export { poolQuestionsMiningMode, poolQuestionsMode, poolQuestionsPushMode, poolQuestionsStampNewborn, POOL_DISCRIMINATOR_MIN_POOL_SIZE, POOL_DISCRIMINATOR_MAX_CANDIDATES, POOL_DISCRIMINATOR_MAX_PUBLIC_CONTEXT_CHARS, POOL_QUESTION_MIN_VOI, POOL_QUESTION_MAX_PENDING_PER_INTENT, poolQuestionsRanking, POOL_RERUN_DEBOUNCE_MS, poolQuestionsVisitTrigger, POOL_VISIT_MINING_DEBOUNCE_MS, } from "../discriminator/discriminator.env.js";
38
38
  export { buildPoolAdjustment, planPoolAdjustments, mergePoolAdjustment, readActivePoolAdjustments, adjustedConfidence, } from "../discriminator/discriminator.adjustments.js";
@@ -176,7 +176,7 @@ export declare const OpportunityGraphState: import("@langchain/langgraph").Annot
176
176
  *
177
177
  * Defaults to 'create' for backward compatibility.
178
178
  */
179
- operationMode: import("@langchain/langgraph").BaseChannel<"delete" | "send" | "update" | "create" | "read" | "create_introduction" | "continue_discovery" | "negotiate_existing" | "approve_introduction", "delete" | "send" | "update" | "create" | "read" | "create_introduction" | "continue_discovery" | "negotiate_existing" | "approve_introduction" | import("@langchain/langgraph").OverwriteValue<"delete" | "send" | "update" | "create" | "read" | "create_introduction" | "continue_discovery" | "negotiate_existing" | "approve_introduction">, unknown>;
179
+ operationMode: import("@langchain/langgraph").BaseChannel<"send" | "update" | "create" | "delete" | "read" | "create_introduction" | "continue_discovery" | "negotiate_existing" | "approve_introduction", "send" | "update" | "create" | "delete" | "read" | "create_introduction" | "continue_discovery" | "negotiate_existing" | "approve_introduction" | import("@langchain/langgraph").OverwriteValue<"send" | "update" | "create" | "delete" | "read" | "create_introduction" | "continue_discovery" | "negotiate_existing" | "approve_introduction">, unknown>;
180
180
  /** Introduction mode: pre-gathered entities (profiles + intents per party). */
181
181
  introductionEntities: import("@langchain/langgraph").BaseChannel<EvaluatorEntity[], EvaluatorEntity[] | import("@langchain/langgraph").OverwriteValue<EvaluatorEntity[]>, unknown>;
182
182
  /** Introduction mode: optional hint from the introducer. */
@@ -380,17 +380,7 @@ export declare const OpportunityGraphState: import("@langchain/langgraph").Annot
380
380
  * negotiate).
381
381
  */
382
382
  discoveryNegotiations: import("@langchain/langgraph").BaseChannel<{
383
- counterpartyId: string;
384
- counterpartyHint: string;
385
383
  indexContext: string;
386
- turns: {
387
- reasoning: string;
388
- action: "propose" | "accept" | "reject" | "counter" | "question" | "outreach" | "withdraw" | "decline" | "ask_user";
389
- suggestedRoles: {
390
- ownUser: "agent" | "patient" | "peer";
391
- otherUser: "agent" | "patient" | "peer";
392
- };
393
- }[];
394
384
  outcome: {
395
385
  reasoning: string;
396
386
  hasOpportunity: boolean;
@@ -400,11 +390,8 @@ export declare const OpportunityGraphState: import("@langchain/langgraph").Annot
400
390
  role: "agent" | "patient" | "peer";
401
391
  }[] | undefined;
402
392
  };
403
- seedAssessmentScore?: number | undefined;
404
- }[], {
405
- counterpartyId: string;
406
393
  counterpartyHint: string;
407
- indexContext: string;
394
+ counterpartyId: string;
408
395
  turns: {
409
396
  reasoning: string;
410
397
  action: "propose" | "accept" | "reject" | "counter" | "question" | "outreach" | "withdraw" | "decline" | "ask_user";
@@ -413,6 +400,9 @@ export declare const OpportunityGraphState: import("@langchain/langgraph").Annot
413
400
  otherUser: "agent" | "patient" | "peer";
414
401
  };
415
402
  }[];
403
+ seedAssessmentScore?: number | undefined;
404
+ }[], {
405
+ indexContext: string;
416
406
  outcome: {
417
407
  reasoning: string;
418
408
  hasOpportunity: boolean;
@@ -422,11 +412,8 @@ export declare const OpportunityGraphState: import("@langchain/langgraph").Annot
422
412
  role: "agent" | "patient" | "peer";
423
413
  }[] | undefined;
424
414
  };
425
- seedAssessmentScore?: number | undefined;
426
- }[] | import("@langchain/langgraph").OverwriteValue<{
427
- counterpartyId: string;
428
415
  counterpartyHint: string;
429
- indexContext: string;
416
+ counterpartyId: string;
430
417
  turns: {
431
418
  reasoning: string;
432
419
  action: "propose" | "accept" | "reject" | "counter" | "question" | "outreach" | "withdraw" | "decline" | "ask_user";
@@ -435,6 +422,9 @@ export declare const OpportunityGraphState: import("@langchain/langgraph").Annot
435
422
  otherUser: "agent" | "patient" | "peer";
436
423
  };
437
424
  }[];
425
+ seedAssessmentScore?: number | undefined;
426
+ }[] | import("@langchain/langgraph").OverwriteValue<{
427
+ indexContext: string;
438
428
  outcome: {
439
429
  reasoning: string;
440
430
  hasOpportunity: boolean;
@@ -444,6 +434,16 @@ export declare const OpportunityGraphState: import("@langchain/langgraph").Annot
444
434
  role: "agent" | "patient" | "peer";
445
435
  }[] | undefined;
446
436
  };
437
+ counterpartyHint: string;
438
+ counterpartyId: string;
439
+ turns: {
440
+ reasoning: string;
441
+ action: "propose" | "accept" | "reject" | "counter" | "question" | "outreach" | "withdraw" | "decline" | "ask_user";
442
+ suggestedRoles: {
443
+ ownUser: "agent" | "patient" | "peer";
444
+ otherUser: "agent" | "patient" | "peer";
445
+ };
446
+ }[];
447
447
  seedAssessmentScore?: number | undefined;
448
448
  }[]>, unknown>;
449
449
  /** Aggregate counters across `discoveryNegotiations`. Built in the negotiate node. */