@indexnetwork/protocol 65.0.0 → 66.0.0-rc.609.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 (26) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/IMPLEMENTATION.md +11 -12
  3. package/STABILITY.md +0 -1
  4. package/dist/index.d.ts +5 -7
  5. package/dist/index.js +3 -6
  6. package/dist/internal/opportunities/opportunity.cards.d.ts +78 -0
  7. package/dist/internal/opportunities/opportunity.cards.js +165 -0
  8. package/dist/internal/opportunities/opportunity.graph.modes.d.ts +0 -1
  9. package/dist/internal/opportunities/opportunity.graph.modes.js +0 -5
  10. package/dist/internal/opportunities/opportunity.labels.d.ts +0 -2
  11. package/dist/internal/opportunities/opportunity.labels.js +0 -2
  12. package/dist/internal/opportunities/opportunity.presentation.d.ts +21 -185
  13. package/dist/internal/opportunities/opportunity.presentation.js +35 -651
  14. package/dist/internal/opportunities/opportunity.utils.d.ts +0 -42
  15. package/dist/internal/opportunities/opportunity.utils.js +0 -102
  16. package/dist/platform/database/capabilities.d.ts +2 -2
  17. package/dist/platform/database/entities.d.ts +1 -1
  18. package/package.json +1 -1
  19. package/dist/internal/opportunities/opportunity.feed-selection.d.ts +0 -24
  20. package/dist/internal/opportunities/opportunity.feed-selection.js +0 -40
  21. package/dist/internal/opportunities/radar/radar.graph.d.ts +0 -230
  22. package/dist/internal/opportunities/radar/radar.graph.js +0 -537
  23. package/dist/internal/opportunities/radar/radar.state.d.ts +0 -88
  24. package/dist/internal/opportunities/radar/radar.state.js +0 -87
  25. package/dist/internal/shared/utils/claim-safety.d.ts +0 -24
  26. package/dist/internal/shared/utils/claim-safety.js +0 -88
@@ -1,537 +0,0 @@
1
- /**
2
- * Radar Graph: Build the opportunity radar view — a flat, presenter-texted
3
- * list of opportunity cards for a viewer, optionally scoped to one intent.
4
- *
5
- * Flow:
6
- * loadOpportunities → checkPresenterCache → [generateCardText if misses]
7
- * → cachePresenterResults → normalizeItems
8
- *
9
- * Uses OpportunityPresenter for card text and caches presenter results via
10
- * OpportunityCache. Responses are a flat `items` array; clients bucket by
11
- * lifecycle status themselves.
12
- */
13
- import { StateGraph, START, END } from '@langchain/langgraph';
14
- import { RadarGraphState } from './radar.state.js';
15
- import { OpportunityPresenter, gatherPresenterContext } from '../opportunity.presentation.js';
16
- import { loadNegotiationContext } from '../negotiation-context.loader.js';
17
- import { canUserSeeOpportunity, isActionableForViewer, selectByComposition } from '../opportunity.utils.js';
18
- import { getPrimaryActionLabel, SECONDARY_ACTION_LABEL } from '../opportunity.labels.js';
19
- import { safeFallbackSummary } from '../opportunity.presentation.js';
20
- import { buildRadarCardPresentationCacheKey } from '../opportunity.presentation.js';
21
- import { protocolLogger } from '../../shared/observability/protocol.logger.js';
22
- import { timed } from '../../shared/observability/performance.js';
23
- import { requestContext } from "../../shared/observability/request-context.js";
24
- const logger = protocolLogger('RadarGraph');
25
- const checkPresenterCacheLog = protocolLogger('RadarGraph:checkPresenterCache');
26
- const cachePresenterResultsLog = protocolLogger('RadarGraph:cachePresenterResults');
27
- const generateCardTextLog = protocolLogger('RadarGraph:generateCardText');
28
- const normalizeItemsLog = protocolLogger('RadarGraph:normalizeItems');
29
- /** Default radar statuses: the lifecycle stages a viewer can act on today. */
30
- export const DEFAULT_RADAR_STATUSES = ['pending'];
31
- // Exhaustive registry — keys must cover every OpportunityStatus union member.
32
- // Adding a new status to OpportunityStatus without adding a key here is a TS error,
33
- // which is the whole point: prevents ALL_OPPORTUNITY_STATUSES from silently drifting.
34
- const OPPORTUNITY_STATUS_REGISTRY = {
35
- negotiating: true,
36
- pending: true,
37
- accepted: true,
38
- rejected: true,
39
- expired: true,
40
- };
41
- /** Full status enumeration. Pass this to `RadarGraphInvokeInput.statuses` to restore pre-Issue-3 (unfiltered) behavior. */
42
- export const ALL_OPPORTUNITY_STATUSES = Object.keys(OPPORTUNITY_STATUS_REGISTRY);
43
- const PRESENTATION_CONCURRENCY = 50;
44
- const MAX_REASONING_SNIPPET_LENGTH = 240;
45
- const RADAR_CACHE_TTL = 24 * 60 * 60; // 24 hours in seconds
46
- /** Pure cache policy for presenter cards; degraded current-request copy retries later. */
47
- export function isRadarPresentationCacheable(card, status) {
48
- return Boolean(status &&
49
- status !== 'negotiating' &&
50
- !card.presentationPending &&
51
- !card._presentationFallback &&
52
- card.name &&
53
- card.name !== 'Unknown');
54
- }
55
- /**
56
- * Strip leading narrator name from remark when the UI already prepends "Name: " to the chip.
57
- * Avoids duplication like "Yankı Ekin Yüksel: Yankı Ekin Yüksel introduced you two..."
58
- * Repeats until no leading name (handles "Name: Name rest").
59
- */
60
- export function stripLeadingNarratorName(remark, narratorName) {
61
- let t = remark.trim();
62
- if (!t || !narratorName.trim())
63
- return remark;
64
- const name = narratorName.trim();
65
- const nameLower = name.toLowerCase();
66
- for (;;) {
67
- const lower = t.toLowerCase();
68
- if (!lower.startsWith(nameLower))
69
- break;
70
- const rest = t.slice(name.length).replace(/^\s*[:,\-–—]\s*/i, '').trim();
71
- if (rest.length === 0 || rest === t)
72
- break;
73
- t = rest;
74
- }
75
- return t;
76
- }
77
- /** Normalize timestamp for sorting; returns numeric ms or 0 for invalid/missing. */
78
- const safeParseDate = (value) => {
79
- if (value == null)
80
- return 0;
81
- if (value instanceof Date) {
82
- const t = value.getTime();
83
- return Number.isFinite(t) ? t : 0;
84
- }
85
- if (typeof value === 'number' && Number.isFinite(value))
86
- return value;
87
- if (typeof value === 'string') {
88
- const t = new Date(value).getTime();
89
- if (!Number.isNaN(t))
90
- return t;
91
- }
92
- return 0;
93
- };
94
- /** Confidence score for sorting (interpretation.confidence or opportunity.confidence). */
95
- const getRawConfidence = (opp) => {
96
- const fromInterp = opp.interpretation?.confidence;
97
- if (typeof fromInterp === 'number' && !Number.isNaN(fromInterp))
98
- return fromInterp;
99
- if (typeof fromInterp === 'string') {
100
- const n = parseFloat(fromInterp);
101
- if (!Number.isNaN(n))
102
- return n;
103
- }
104
- const fromRow = opp.confidence;
105
- if (typeof fromRow === 'number' && !Number.isNaN(fromRow))
106
- return fromRow;
107
- if (typeof fromRow === 'string') {
108
- const n = parseFloat(fromRow);
109
- if (!Number.isNaN(n))
110
- return n;
111
- }
112
- return 0;
113
- };
114
- /** Unique non-viewer userIds for an opportunity (actors can repeat). */
115
- const getUniqueCounterpartUserIds = (opp, viewerId) => {
116
- const ids = new Set();
117
- for (const a of opp.actors) {
118
- if (a.userId !== viewerId && a.userId) {
119
- ids.add(a.userId);
120
- }
121
- }
122
- return ids;
123
- };
124
- const pickDisplayCounterpartActor = (opportunity, viewerId) => {
125
- const candidates = opportunity.actors.filter((actor) => actor.userId !== viewerId);
126
- if (candidates.length === 0) {
127
- return null;
128
- }
129
- // Prefer direct counterpart roles when available, then stable sort by user id.
130
- const rolePriority = new Map([
131
- ['patient', 0],
132
- ['party', 1],
133
- ['agent', 2],
134
- ['peer', 3],
135
- ]);
136
- const sorted = [...candidates].sort((a, b) => {
137
- const aPriority = rolePriority.get(a.role) ?? 99;
138
- const bPriority = rolePriority.get(b.role) ?? 99;
139
- if (aPriority !== bPriority)
140
- return aPriority - bPriority;
141
- return a.userId.localeCompare(b.userId);
142
- });
143
- return sorted[0] ?? null;
144
- };
145
- export class RadarGraphFactory {
146
- constructor(database, cache) {
147
- this.deps = { database, cache, presenter: new OpportunityPresenter() };
148
- }
149
- createGraph() {
150
- const deps = this.deps;
151
- const graph = new StateGraph(RadarGraphState)
152
- .addNode('loadOpportunities', (state) => loadOpportunitiesNode(state, deps))
153
- .addNode('checkPresenterCache', (state) => checkPresenterCacheNode(state, deps))
154
- .addNode('generateCardText', (state) => generateCardTextNode(state, deps))
155
- .addNode('cachePresenterResults', (state) => cachePresenterResultsNode(state, deps))
156
- .addNode('normalizeItems', normalizeItemsNode)
157
- .addEdge(START, 'loadOpportunities')
158
- .addEdge('loadOpportunities', 'checkPresenterCache')
159
- .addConditionalEdges('checkPresenterCache', (state) => shouldGenerateCards(state, deps), {
160
- generate: 'generateCardText',
161
- skip: 'cachePresenterResults',
162
- })
163
- .addEdge('generateCardText', 'cachePresenterResults')
164
- .addEdge('cachePresenterResults', 'normalizeItems')
165
- .addEdge('normalizeItems', END);
166
- return graph.compile();
167
- }
168
- }
169
- export async function loadOpportunitiesNode(state, deps) {
170
- return timed("RadarGraph.loadOpportunities", async () => {
171
- if (!state.userId) {
172
- return { error: 'userId is required' };
173
- }
174
- try {
175
- // Minimum of 50 ensures enough candidates across all radar categories
176
- // (connection, expired) for selectByComposition to fill
177
- // its soft targets, even after visibility filtering and dedup.
178
- const fetchLimit = Math.min(150, Math.max(50, state.limit * 3));
179
- const statuses = state.statuses ?? DEFAULT_RADAR_STATUSES;
180
- const options = {
181
- limit: fetchLimit,
182
- statuses,
183
- };
184
- if (state.networkId)
185
- options.networkId = state.networkId;
186
- if (state.scopeType === 'intent' && state.scopeId) {
187
- options.scopeType = 'intent';
188
- options.scopeId = state.scopeId;
189
- }
190
- // Do not pass conversationId: radar view excludes draft opportunities (chat-only drafts).
191
- const raw = await deps.database.getOpportunitiesForUser(state.userId, options);
192
- const visible = raw.filter((opp) => canUserSeeOpportunity(opp.actors, opp.status, state.userId));
193
- // Actionability only gates the live statuses a viewer could act on:
194
- // latent/pending cards the viewer cannot act on are noise, but
195
- // terminal/internal statuses (accepted, rejected, expired, negotiating,
196
- // draft) are deliberate history — when a caller explicitly
197
- // requests them via `statuses`, they must pass through (they are never
198
- // actionable by rule 5, so filtering them here would return nothing).
199
- // The requested-status membership check is defense-in-depth: rows
200
- // outside the requested set are dropped even if the adapter drifts.
201
- const requestedStatuses = new Set(statuses);
202
- const visibleForRadar = visible.filter((opp) => {
203
- if (!requestedStatuses.has(opp.status))
204
- return false;
205
- if (opp.status === 'pending') {
206
- return isActionableForViewer(opp.actors, opp.status, state.userId, opp.committedActorIds ?? []);
207
- }
208
- return true;
209
- });
210
- const explicitStatuses = (state.statuses?.length ?? 0) > 0;
211
- if (explicitStatuses) {
212
- // Lifecycle view (e.g. intent radar): newest-created first so status
213
- // transitions do not reorder cards merely by touching updatedAt.
214
- // Counterpart dedup still keeps each person's newest opportunity; no
215
- // composition capping because the caller wants the full pipeline.
216
- const newestFirst = [...visibleForRadar].sort((a, b) => safeParseDate(b.createdAt) - safeParseDate(a.createdAt));
217
- const seenIds = new Set();
218
- const dedupedByCounterpart = newestFirst.filter((opp) => {
219
- const counterpartIds = getUniqueCounterpartUserIds(opp, state.userId);
220
- const hasOverlap = [...counterpartIds].some((id) => seenIds.has(id));
221
- if (hasOverlap)
222
- return false;
223
- for (const id of counterpartIds)
224
- seenIds.add(id);
225
- return true;
226
- });
227
- return { opportunities: dedupedByCounterpart.slice(0, state.limit) };
228
- }
229
- const sorted = [...visibleForRadar].sort((a, b) => {
230
- const confA = getRawConfidence(a);
231
- const confB = getRawConfidence(b);
232
- if (confB !== confA)
233
- return confB - confA;
234
- const aTime = safeParseDate(a.updatedAt);
235
- const bTime = safeParseDate(b.updatedAt);
236
- return bTime - aTime;
237
- });
238
- const seenUserIds = new Set();
239
- const deduped = sorted.filter((opp) => {
240
- const counterpartIds = getUniqueCounterpartUserIds(opp, state.userId);
241
- const hasOverlap = [...counterpartIds].some((id) => seenUserIds.has(id));
242
- if (hasOverlap)
243
- return false;
244
- for (const id of counterpartIds)
245
- seenUserIds.add(id);
246
- return true;
247
- });
248
- const opportunities = selectByComposition(deduped, state.userId);
249
- return { opportunities };
250
- }
251
- catch (e) {
252
- logger.error('RadarGraph loadOpportunities failed', { error: e });
253
- return { error: 'Failed to load opportunities', opportunities: [] };
254
- }
255
- });
256
- }
257
- export async function checkPresenterCacheNode(state, deps) {
258
- return timed("RadarGraph.checkPresenterCache", async () => {
259
- const { opportunities, userId, scopeId } = state;
260
- if (opportunities.length === 0) {
261
- return { cachedCards: new Map(), uncachedOpportunities: [] };
262
- }
263
- if (state.noCache) {
264
- checkPresenterCacheLog.verbose('noCache=true, skipping cache');
265
- return { cachedCards: new Map(), uncachedOpportunities: opportunities };
266
- }
267
- try {
268
- // Negotiating cards are templated (no LLM call) and their text
269
- // depends on the live turn count, which changes between requests
270
- // without changing the opportunity status. Skip cache entirely
271
- // for them so each render reflects the current turn.
272
- //
273
- // For all other statuses, include status in the key so status
274
- // transitions (e.g. negotiating → pending) don't serve stale cards.
275
- const cacheable = opportunities.filter((opp) => opp.status !== 'negotiating');
276
- const liveNegotiating = opportunities.filter((opp) => opp.status === 'negotiating');
277
- const keys = cacheable.map((opp) => buildRadarCardPresentationCacheKey(opp.id, opp.status, userId, scopeId));
278
- const results = keys.length > 0 ? await deps.cache.mget(keys) : [];
279
- const cachedCards = new Map();
280
- const uncachedOpportunities = [...liveNegotiating];
281
- for (let i = 0; i < cacheable.length; i++) {
282
- const cached = results[i];
283
- if (cached) {
284
- const originalIndex = opportunities.indexOf(cacheable[i]);
285
- // Stamp the live status: pre-status cache entries lack the field,
286
- // and the key already guarantees it matches the current status.
287
- cachedCards.set(cacheable[i].id, {
288
- ...cached,
289
- status: cacheable[i].status,
290
- _cardIndex: originalIndex,
291
- });
292
- }
293
- else {
294
- uncachedOpportunities.push(cacheable[i]);
295
- }
296
- }
297
- checkPresenterCacheLog.verbose('', {
298
- total: opportunities.length,
299
- cacheHits: cachedCards.size,
300
- cacheMisses: uncachedOpportunities.length,
301
- });
302
- return { cachedCards, uncachedOpportunities };
303
- }
304
- catch (e) {
305
- checkPresenterCacheLog.warn('cache unavailable, skipping', { error: e });
306
- return { cachedCards: new Map(), uncachedOpportunities: opportunities };
307
- }
308
- });
309
- }
310
- export function shouldGenerateCards(state, _deps) {
311
- if (state.uncachedOpportunities.length > 0) {
312
- return 'generate';
313
- }
314
- logger.verbose('All presenter results cached, skipping generation');
315
- return 'skip';
316
- }
317
- export async function generateCardTextNode(state, deps) {
318
- return timed("RadarGraph.generateCardText", async () => {
319
- const opportunities = state.uncachedOpportunities.length > 0
320
- ? state.uncachedOpportunities
321
- : state.opportunities;
322
- generateCardTextLog.verbose('entry', { opportunitiesLength: opportunities.length, userId: state.userId });
323
- if (opportunities.length === 0) {
324
- generateCardTextLog.verbose('exit', { totalOpportunities: 0 });
325
- return { cards: [], agentTimings: [], meta: { totalOpportunities: 0 } };
326
- }
327
- const db = deps.database;
328
- const cards = [];
329
- const relevantActorIds = new Set();
330
- for (const opp of opportunities) {
331
- for (const a of opp.actors) {
332
- if (a.userId)
333
- relevantActorIds.add(a.userId);
334
- }
335
- }
336
- const userEntries = await Promise.all(Array.from(relevantActorIds).map(async (userId) => {
337
- try {
338
- const user = await deps.database.getUser(userId);
339
- return [userId, user ?? null];
340
- }
341
- catch {
342
- return [userId, null];
343
- }
344
- }));
345
- const userMap = new Map(userEntries);
346
- const oppIndexMap = new Map(state.opportunities.map((opp, idx) => [opp.id, idx]));
347
- const agentTimingsAccum = [];
348
- for (let i = 0; i < opportunities.length; i += PRESENTATION_CONCURRENCY) {
349
- const chunk = opportunities.slice(i, i + PRESENTATION_CONCURRENCY);
350
- const chunkCards = await Promise.all(chunk.map(async (opportunity, offset) => {
351
- const cardIndex = oppIndexMap.get(opportunity.id) ?? (i + offset);
352
- const viewerActor = opportunity.actors.find((a) => a.userId === state.userId);
353
- const viewerRole = viewerActor?.role ?? 'party';
354
- const preferredActor = pickDisplayCounterpartActor(opportunity, state.userId)
355
- ?? opportunity.actors.find((a) => a.userId !== state.userId);
356
- const actorWithProfile = opportunity.actors.find((a) => a.userId !== state.userId && !!userMap.get(a.userId));
357
- const otherActor = (preferredActor && userMap.get(preferredActor.userId))
358
- ? preferredActor
359
- : (actorWithProfile ?? preferredActor);
360
- const otherUser = otherActor ? userMap.get(otherActor.userId) ?? null : null;
361
- let userName = otherUser?.name ?? 'Unknown';
362
- // Fallback to profile identity name when users.name is missing (e.g. profile has display name, users row does not)
363
- if ((userName === 'Unknown' || !userName?.trim()) && otherActor?.userId && db.getProfile) {
364
- const profile = await db.getProfile(otherActor.userId).catch((err) => {
365
- logger.debug('getProfile fallback failed', { otherActorUserId: otherActor.userId, error: err });
366
- return null;
367
- });
368
- const profileName = profile?.identity?.name?.trim();
369
- if (profileName)
370
- userName = profileName;
371
- }
372
- // Unresolvable display counterpart (deleted user: no users row, no
373
- // profile fallback). Drop the card entirely instead of rendering an
374
- // "Unknown" placeholder: such cards are unusable, excluded from the
375
- // deps.presenter cache (see cachePresenterResults), and would otherwise
376
- // trigger a fresh deps.presenter LLM call on every request — a permanent
377
- // cache miss that keeps the whole radar slow (~9s per load).
378
- if (userName === 'Unknown' || !userName?.trim()) {
379
- logger.verbose('[RadarGraph:generateCardText] dropping card with unresolvable counterpart', {
380
- opportunityId: opportunity.id,
381
- otherActorUserId: otherActor?.userId,
382
- });
383
- return null;
384
- }
385
- const userAvatar = otherUser?.avatar ?? null;
386
- // Shared sanitization standard (UUID strip, viewer-centric rewrite,
387
- // boundary truncation) — raw reasoning must never render verbatim.
388
- const reasoningSnippet = safeFallbackSummary(typeof opportunity.interpretation?.reasoning === 'string'
389
- ? opportunity.interpretation.reasoning
390
- : '', {
391
- counterpartName: userName !== 'Unknown' ? userName : undefined,
392
- maxChars: MAX_REASONING_SNIPPET_LENGTH,
393
- emptyText: 'A promising connection.',
394
- });
395
- // Skeleton presentation: return an identity-only card without the
396
- // deps.presenter LLM or negotiation-context load. Name resolution and
397
- // the unresolvable-counterpart drop above still apply, so the card
398
- // set matches what the follow-up full request will return.
399
- if (state.presentation === 'skeleton') {
400
- return {
401
- opportunityId: opportunity.id,
402
- status: opportunity.status,
403
- userId: otherActor?.userId ?? '',
404
- name: userName,
405
- avatar: userAvatar,
406
- mainText: '',
407
- cta: '',
408
- primaryActionLabel: getPrimaryActionLabel(viewerRole),
409
- secondaryActionLabel: SECONDARY_ACTION_LABEL,
410
- mutualIntentsLabel: 'Shared interests',
411
- viewerRole,
412
- presentationPending: true,
413
- _cardIndex: cardIndex,
414
- };
415
- }
416
- const fallbackCard = (outcomeReasoning) => ({
417
- opportunityId: opportunity.id,
418
- status: opportunity.status,
419
- userId: otherActor?.userId ?? '',
420
- name: userName,
421
- avatar: userAvatar,
422
- mainText: outcomeReasoning ?? reasoningSnippet,
423
- cta: 'Take a look and decide whether to reach out.',
424
- primaryActionLabel: getPrimaryActionLabel(viewerRole),
425
- secondaryActionLabel: SECONDARY_ACTION_LABEL,
426
- mutualIntentsLabel: 'Shared interests',
427
- narratorChip: { name: 'Index', text: 'Worth a look.' },
428
- viewerRole,
429
- _presentationFallback: true,
430
- _cardIndex: cardIndex,
431
- });
432
- try {
433
- const [ctx, negotiationContext] = await Promise.all([
434
- gatherPresenterContext(db, opportunity, state.userId, otherActor?.userId, state.scopeId),
435
- loadNegotiationContext(db, opportunity.id, opportunity.status, state.userId),
436
- ]);
437
- const presenterInput = {
438
- ...ctx,
439
- mutualIntentCount: undefined,
440
- opportunityStatus: opportunity.status,
441
- ...(negotiationContext ? { negotiationContext } : {}),
442
- };
443
- const _traceEmitterPresenter = requestContext.getStore()?.traceEmitter;
444
- const presenterStart = Date.now();
445
- _traceEmitterPresenter?.({ type: "agent_start", name: "opportunity-presenter" });
446
- const presentation = await deps.presenter.presentCard(presenterInput);
447
- const _presenterDuration = Date.now() - presenterStart;
448
- agentTimingsAccum.push({ name: 'opportunity.presenter', durationMs: _presenterDuration });
449
- _traceEmitterPresenter?.({ type: "agent_end", name: "opportunity-presenter", durationMs: _presenterDuration, summary: `Presented: ${userName}` });
450
- if (presentation.isFallback) {
451
- return fallbackCard();
452
- }
453
- // Every card is system-discovered now: one narrator.
454
- const narratorChip = { name: 'Index', text: presentation.narratorRemark };
455
- return {
456
- opportunityId: opportunity.id,
457
- status: opportunity.status,
458
- userId: otherActor?.userId ?? '',
459
- name: userName,
460
- avatar: userAvatar,
461
- mainText: presentation.personalizedSummary,
462
- cta: presentation.suggestedAction,
463
- headline: presentation.headline,
464
- primaryActionLabel: getPrimaryActionLabel(viewerRole),
465
- secondaryActionLabel: SECONDARY_ACTION_LABEL,
466
- mutualIntentsLabel: presentation.mutualIntentsLabel,
467
- narratorChip,
468
- viewerRole,
469
- _cardIndex: cardIndex,
470
- };
471
- }
472
- catch (e) {
473
- logger.warn('RadarGraph presenter failed for opportunity', { opportunityId: opportunity.id, error: e });
474
- return fallbackCard();
475
- }
476
- }));
477
- cards.push(...chunkCards.filter((c) => c !== null));
478
- }
479
- generateCardTextLog.verbose('exit', { totalOpportunities: state.opportunities.length });
480
- return {
481
- cards,
482
- agentTimings: agentTimingsAccum,
483
- meta: { totalOpportunities: state.opportunities.length },
484
- };
485
- });
486
- }
487
- export async function cachePresenterResultsNode(state, deps) {
488
- return timed("RadarGraph.cachePresenterResults", async () => {
489
- const { cards, cachedCards, userId, opportunities, scopeId } = state;
490
- // Only cache cards that weren't already from cache
491
- const newCards = cards.filter((card) => !cachedCards.has(card.opportunityId));
492
- const statusById = new Map(opportunities.map((opp) => [opp.id, opp.status]));
493
- try {
494
- await Promise.all(newCards.map((card) => {
495
- const status = statusById.get(card.opportunityId);
496
- // Negotiating, skeleton, fallback, and unresolved-name cards are
497
- // safe for the current response but must not become 24h entries.
498
- if (!status || !isRadarPresentationCacheable(card, status))
499
- return Promise.resolve();
500
- return deps.cache.set(buildRadarCardPresentationCacheKey(card.opportunityId, status, userId, scopeId), card, { ttl: RADAR_CACHE_TTL });
501
- }));
502
- }
503
- catch (e) {
504
- cachePresenterResultsLog.warn('cache write failed, continuing', { error: e });
505
- }
506
- // Merge cached cards into full card list
507
- const allCards = [...cards];
508
- for (const [oppId, cachedCard] of cachedCards) {
509
- if (!cards.some((card) => card.opportunityId === oppId)) {
510
- allCards.push(cachedCard);
511
- }
512
- }
513
- // Re-sort by _cardIndex to maintain original ordering
514
- allCards.sort((a, b) => a._cardIndex - b._cardIndex);
515
- cachePresenterResultsLog.verbose('', {
516
- newlyCached: newCards.length,
517
- totalCards: allCards.length,
518
- });
519
- return {
520
- cards: allCards,
521
- meta: { totalOpportunities: state.opportunities.length },
522
- };
523
- });
524
- }
525
- export async function normalizeItemsNode(state) {
526
- return timed("RadarGraph.normalizeItems", async () => {
527
- normalizeItemsLog.verbose('entry', { cardsLength: state.cards.length });
528
- const createdAtById = new Map(state.opportunities.map((opportunity) => [opportunity.id, opportunity.createdAt.toISOString()]));
529
- const items = state.cards.map((card) => {
530
- const { _cardIndex, _presentationFallback, ...rest } = card;
531
- return { ...rest, createdAt: createdAtById.get(card.opportunityId) };
532
- });
533
- const meta = { totalOpportunities: state.opportunities.length };
534
- normalizeItemsLog.verbose('exit', { totalOpportunities: meta.totalOpportunities, totalItems: items.length });
535
- return { items, meta };
536
- });
537
- }
@@ -1,88 +0,0 @@
1
- import type { Opportunity, OpportunityStatus } from '../../../platform/database.js';
2
- import type { DebugMetaAgent } from "../../../protocol/core.js";
3
- /**
4
- * Radar card item: one opportunity with full presenter-driven display contract.
5
- */
6
- export interface RadarCardItem {
7
- opportunityId: string;
8
- createdAt?: string;
9
- /** Lifecycle status of the underlying opportunity at render time (client bucketing, e.g. intent radar). */
10
- status?: OpportunityStatus;
11
- userId: string;
12
- name: string;
13
- avatar: string | null;
14
- mainText: string;
15
- cta: string;
16
- headline?: string;
17
- /** Presenter-generated; primary button (accept) and secondary button (dismiss). */
18
- primaryActionLabel: string;
19
- secondaryActionLabel: string;
20
- /** Presenter-generated subtitle under the other party name (e.g. "1 mutual intent"). */
21
- mutualIntentsLabel: string;
22
- /** Narrator chip for human-introduced opportunities; avatar set when narrator is a user */
23
- narratorChip?: {
24
- name: string;
25
- text: string;
26
- avatar?: string | null;
27
- userId?: string;
28
- };
29
- /** Viewer's role in this opportunity (e.g. 'party', 'agent', 'patient', 'peer'). */
30
- viewerRole?: string;
31
- /**
32
- * True when this card was produced by a skeleton-presentation run: identity
33
- * fields (name/avatar/status) are real but mainText/cta are empty because the
34
- * presenter LLM was skipped. Clients render a shimmer body and re-fetch the
35
- * full view. Skeleton cards are never written to the presenter cache.
36
- */
37
- presentationPending?: boolean;
38
- /** Internal marker: safe deterministic fallback rendered, but must not be cached. */
39
- _presentationFallback?: boolean;
40
- /** Internal: original position in the loaded opportunity list, for stable ordering. */
41
- _cardIndex: number;
42
- }
43
- /** Card item as returned in API responses (no internal fields). */
44
- export type RadarResponseItem = Omit<RadarCardItem, '_cardIndex' | '_presentationFallback'>;
45
- /**
46
- * Radar Graph State (Annotation-based).
47
- * Flow: loadOpportunities → checkPresenterCache → [generateCardText if misses]
48
- * → cachePresenterResults → normalizeItems.
49
- */
50
- export declare const RadarGraphState: import("@langchain/langgraph").AnnotationRoot<{
51
- userId: import("@langchain/langgraph").BaseChannel<string, string | import("@langchain/langgraph").OverwriteValue<string>, unknown>;
52
- networkId: import("@langchain/langgraph").BaseChannel<string | undefined, string | import("@langchain/langgraph").OverwriteValue<string | undefined> | undefined, unknown>;
53
- scopeType: import("@langchain/langgraph").BaseChannel<"intent" | undefined, "intent" | import("@langchain/langgraph").OverwriteValue<"intent" | undefined> | undefined, unknown>;
54
- scopeId: import("@langchain/langgraph").BaseChannel<string | undefined, string | import("@langchain/langgraph").OverwriteValue<string | undefined> | undefined, unknown>;
55
- limit: import("@langchain/langgraph").BaseChannel<number, number | import("@langchain/langgraph").OverwriteValue<number>, unknown>;
56
- /** When true, bypass the presenter Redis cache. */
57
- noCache: import("@langchain/langgraph").BaseChannel<boolean, boolean | import("@langchain/langgraph").OverwriteValue<boolean>, unknown>;
58
- /**
59
- * Presentation depth. 'full' (default) runs the presenter LLM for cache
60
- * misses. 'skeleton' skips it: uncached cards come back with resolved
61
- * identity (name/avatar/status) and `presentationPending: true`, cached
62
- * cards come back complete.
63
- */
64
- presentation: import("@langchain/langgraph").BaseChannel<"full" | "skeleton", "full" | "skeleton" | import("@langchain/langgraph").OverwriteValue<"full" | "skeleton">, unknown>;
65
- /** Optional status filter. When undefined, the graph uses `DEFAULT_RADAR_STATUSES`. */
66
- statuses: import("@langchain/langgraph").BaseChannel<OpportunityStatus[] | undefined, OpportunityStatus[] | import("@langchain/langgraph").OverwriteValue<OpportunityStatus[] | undefined> | undefined, unknown>;
67
- /** Raw opportunities visible to the viewer (after visibility filter). */
68
- opportunities: import("@langchain/langgraph").BaseChannel<Opportunity[], Opportunity[] | import("@langchain/langgraph").OverwriteValue<Opportunity[]>, unknown>;
69
- /** Cards with presenter output and narrator chip. */
70
- cards: import("@langchain/langgraph").BaseChannel<RadarCardItem[], RadarCardItem[] | import("@langchain/langgraph").OverwriteValue<RadarCardItem[]>, unknown>;
71
- /** Final items for response (internal fields stripped). */
72
- items: import("@langchain/langgraph").BaseChannel<RadarResponseItem[], RadarResponseItem[] | import("@langchain/langgraph").OverwriteValue<RadarResponseItem[]>, unknown>;
73
- /** Presenter results retrieved from cache (opportunityId → RadarCardItem). */
74
- cachedCards: import("@langchain/langgraph").BaseChannel<Map<string, RadarCardItem>, Map<string, RadarCardItem> | import("@langchain/langgraph").OverwriteValue<Map<string, RadarCardItem>>, unknown>;
75
- /** Opportunities that had no cache hit and need presenter generation. */
76
- uncachedOpportunities: import("@langchain/langgraph").BaseChannel<Opportunity[], Opportunity[] | import("@langchain/langgraph").OverwriteValue<Opportunity[]>, unknown>;
77
- error: import("@langchain/langgraph").BaseChannel<string | undefined, string | import("@langchain/langgraph").OverwriteValue<string | undefined> | undefined, unknown>;
78
- /** Meta for response (e.g. totalOpportunities). */
79
- meta: import("@langchain/langgraph").BaseChannel<{
80
- totalOpportunities: number;
81
- }, {
82
- totalOpportunities: number;
83
- } | import("@langchain/langgraph").OverwriteValue<{
84
- totalOpportunities: number;
85
- }>, unknown>;
86
- /** Timing records for each agent invocation within this graph run. */
87
- agentTimings: import("@langchain/langgraph").BaseChannel<DebugMetaAgent[], DebugMetaAgent[] | import("@langchain/langgraph").OverwriteValue<DebugMetaAgent[]>, unknown>;
88
- }>;