@indexnetwork/protocol 14.3.2-rc.477.1 → 17.0.0-rc.479.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.
- package/CHANGELOG.md +141 -0
- package/IMPLEMENTATION.md +50 -52
- package/STABILITY.md +3 -3
- package/dist/chat/chat.agent.js +2 -3
- package/dist/chat/chat.graph.d.ts +41 -1
- package/dist/chat/chat.graph.js +108 -127
- package/dist/chat/negotiator.persona.d.ts +1 -1
- package/dist/chat/negotiator.persona.js +1 -4
- package/dist/chat/negotiator.prompt.js +1 -2
- package/dist/chat/onboarding.persona.js +0 -1
- package/dist/chat/reporter.persona.js +0 -1
- package/dist/chat/signal.persona.js +0 -1
- package/dist/contacts/application/contact.tools.d.ts +6 -9
- package/dist/contacts/application/contact.tools.js +11 -92
- package/dist/contacts/application/index.d.ts +2 -14
- package/dist/contacts/application/index.js +2 -13
- package/dist/contacts/domain/contact.types.d.ts +0 -23
- package/dist/contacts/domain/index.d.ts +2 -6
- package/dist/contacts/index.d.ts +1 -1
- package/dist/contacts/index.js +1 -1
- package/dist/contacts/ports/contact.repository.port.d.ts +5 -12
- package/dist/contacts/ports/contact.repository.port.js +4 -6
- package/dist/contacts/ports/contact.tools.port.d.ts +1 -3
- package/dist/contacts/ports/contact.tools.port.js +1 -2
- package/dist/contacts/ports/index.d.ts +1 -1
- package/dist/contacts/ports/index.js +1 -1
- package/dist/discovery/hyde.frame.d.ts +4 -4
- package/dist/discovery/hyde.graph.d.ts +71 -12
- package/dist/discovery/hyde.graph.js +337 -335
- package/dist/discovery/lens.inferrer.d.ts +6 -6
- package/dist/enrichment/enrichment.graph.d.ts +127 -9
- package/dist/enrichment/enrichment.graph.js +633 -655
- package/dist/enrichment/enrichment.state.d.ts +1 -1
- package/dist/enrichment/enrichment.tools.context-read.d.ts +10 -0
- package/dist/enrichment/enrichment.tools.context-read.js +353 -0
- package/dist/enrichment/enrichment.tools.context-write.d.ts +9 -0
- package/dist/enrichment/enrichment.tools.context-write.js +417 -0
- package/dist/enrichment/enrichment.tools.d.ts +11 -1
- package/dist/enrichment/enrichment.tools.helpers.d.ts +139 -0
- package/dist/enrichment/enrichment.tools.helpers.js +234 -0
- package/dist/enrichment/enrichment.tools.js +16 -975
- package/dist/index.d.ts +27 -50
- package/dist/index.js +17 -28
- package/dist/intents/application/intent.graph.d.ts +71 -67
- package/dist/intents/application/intent.graph.execute.d.ts +59 -0
- package/dist/intents/application/intent.graph.execute.js +301 -0
- package/dist/intents/application/intent.graph.infer.d.ts +44 -0
- package/dist/intents/application/intent.graph.infer.js +97 -0
- package/dist/intents/application/intent.graph.js +96 -888
- package/dist/intents/application/intent.graph.reconcile.d.ts +71 -0
- package/dist/intents/application/intent.graph.reconcile.js +274 -0
- package/dist/intents/application/intent.graph.shared.d.ts +70 -0
- package/dist/intents/application/intent.graph.shared.js +153 -0
- package/dist/intents/domain/intent.state.d.ts +1 -1
- package/dist/maintenance/maintenance.graph.d.ts +66 -3
- package/dist/maintenance/maintenance.graph.js +155 -156
- package/dist/mcp/mcp.authorization-policy.d.ts +24 -28
- package/dist/mcp/mcp.authorization-policy.js +14 -34
- package/dist/mcp/mcp.server.d.ts +3 -3
- package/dist/mcp/mcp.server.js +14 -20
- package/dist/negotiations/application/negotiation.candidates.d.ts +83 -0
- package/dist/negotiations/application/negotiation.candidates.js +162 -0
- package/dist/negotiations/application/negotiation.graph.d.ts +91 -157
- package/dist/negotiations/application/negotiation.graph.finalize.d.ts +5 -0
- package/dist/negotiations/application/negotiation.graph.finalize.js +280 -0
- package/dist/negotiations/application/negotiation.graph.init.d.ts +54 -0
- package/dist/negotiations/application/negotiation.graph.init.js +227 -0
- package/dist/negotiations/application/negotiation.graph.js +69 -1388
- package/dist/negotiations/application/negotiation.graph.screen.d.ts +23 -0
- package/dist/negotiations/application/negotiation.graph.screen.js +108 -0
- package/dist/negotiations/application/negotiation.graph.shared.d.ts +75 -0
- package/dist/negotiations/application/negotiation.graph.shared.js +125 -0
- package/dist/negotiations/application/negotiation.graph.turn.d.ts +105 -0
- package/dist/negotiations/application/negotiation.graph.turn.js +484 -0
- package/dist/negotiations/domain/negotiation.state.d.ts +1 -1
- package/dist/negotiations/domain/negotiation.state.js +0 -1
- package/dist/networks/application/indexer.graph.d.ts +165 -7
- package/dist/networks/application/indexer.graph.js +339 -388
- package/dist/networks/application/indexer.state.d.ts +1 -1
- package/dist/networks/application/membership.graph.d.ts +83 -5
- package/dist/networks/application/membership.graph.js +177 -207
- package/dist/networks/application/network.graph.d.ts +148 -5
- package/dist/networks/application/network.graph.js +249 -278
- package/dist/networks/domain/membership.state.d.ts +1 -1
- package/dist/networks/domain/network.state.d.ts +1 -1
- package/dist/opportunities/application/delivery-card.cache.d.ts +1 -1
- package/dist/opportunities/application/delivery-card.cache.js +2 -2
- package/dist/opportunities/application/index.d.ts +2 -2
- package/dist/opportunities/application/index.js +2 -2
- package/dist/opportunities/application/opportunity.evaluator.js +1 -1
- package/dist/opportunities/application/opportunity.graph.d.ts +686 -473
- package/dist/opportunities/application/opportunity.graph.discovery-strategies.d.ts +109 -0
- package/dist/opportunities/application/opportunity.graph.discovery-strategies.js +451 -0
- package/dist/opportunities/application/opportunity.graph.discovery.d.ts +69 -0
- package/dist/opportunities/application/opportunity.graph.discovery.js +397 -0
- package/dist/opportunities/application/opportunity.graph.evaluation.d.ts +82 -0
- package/dist/opportunities/application/opportunity.graph.evaluation.js +608 -0
- package/dist/opportunities/application/opportunity.graph.js +120 -3690
- package/dist/opportunities/application/opportunity.graph.modes.d.ts +538 -0
- package/dist/opportunities/application/opportunity.graph.modes.js +538 -0
- package/dist/opportunities/application/opportunity.graph.negotiate.d.ts +109 -0
- package/dist/opportunities/application/opportunity.graph.negotiate.js +392 -0
- package/dist/opportunities/application/opportunity.graph.persist-node.d.ts +93 -0
- package/dist/opportunities/application/opportunity.graph.persist-node.js +765 -0
- package/dist/opportunities/application/opportunity.graph.prep.d.ts +150 -0
- package/dist/opportunities/application/opportunity.graph.prep.js +381 -0
- package/dist/opportunities/application/opportunity.graph.shared.d.ts +173 -0
- package/dist/opportunities/application/opportunity.graph.shared.js +199 -0
- package/dist/opportunities/application/opportunity.presentation.d.ts +399 -0
- package/dist/opportunities/application/{opportunity.presenter.js → opportunity.presentation.js} +735 -9
- package/dist/opportunities/application/opportunity.tools.cards.d.ts +152 -0
- package/dist/opportunities/application/opportunity.tools.cards.js +235 -0
- package/dist/opportunities/application/opportunity.tools.d.ts +8 -114
- package/dist/opportunities/application/opportunity.tools.js +28 -711
- package/dist/opportunities/application/opportunity.tools.list.d.ts +10 -0
- package/dist/opportunities/application/opportunity.tools.list.js +493 -0
- package/dist/opportunities/domain/index.d.ts +3 -3
- package/dist/opportunities/domain/index.js +3 -3
- package/dist/opportunities/domain/opportunity.state.d.ts +19 -19
- package/dist/opportunities/index.d.ts +6 -6
- package/dist/opportunities/index.js +4 -4
- package/dist/opportunities/ports/opportunity.tools.port.d.ts +1 -1
- package/dist/opportunities/radar/radar.graph.d.ts +57 -13
- package/dist/opportunities/radar/radar.graph.js +470 -471
- package/dist/premises/premise.graph.d.ts +88 -20
- package/dist/premises/premise.graph.js +207 -218
- package/dist/questions/domain/question.schema.d.ts +40 -40
- package/dist/shared/agent/tool.factory.js +0 -8
- package/dist/shared/agent/tool.helpers.d.ts +21 -45
- package/dist/shared/agent/tool.helpers.js +1 -2
- package/dist/shared/agent/tool.registry.d.ts +4 -3
- package/dist/shared/agent/tool.registry.js +2 -4
- package/dist/shared/agent/tool.runtime.js +0 -2
- package/dist/shared/agent/utility.tools.js +9 -13
- package/dist/shared/interfaces/database.capabilities.d.ts +100 -0
- package/dist/shared/interfaces/database.capabilities.js +7 -0
- package/dist/shared/interfaces/database.entities.d.ts +533 -0
- package/dist/shared/interfaces/database.entities.js +10 -0
- package/dist/shared/interfaces/database.identity-queries.d.ts +294 -0
- package/dist/shared/interfaces/database.identity-queries.js +4 -0
- package/dist/shared/interfaces/database.interface.d.ts +15 -2277
- package/dist/shared/interfaces/database.interface.js +8 -0
- package/dist/shared/interfaces/database.member-queries.d.ts +221 -0
- package/dist/shared/interfaces/database.member-queries.js +4 -0
- package/dist/shared/interfaces/database.negotiation.d.ts +305 -0
- package/dist/shared/interfaces/database.negotiation.js +26 -0
- package/dist/shared/interfaces/database.network-queries.d.ts +277 -0
- package/dist/shared/interfaces/database.network-queries.js +4 -0
- package/dist/shared/interfaces/database.opportunity-queries.d.ts +285 -0
- package/dist/shared/interfaces/database.opportunity-queries.js +4 -0
- package/dist/shared/interfaces/database.port.d.ts +322 -0
- package/dist/shared/interfaces/database.port.js +29 -0
- package/dist/shared/schemas/discovery-question.schema.d.ts +14 -14
- package/dist/shared/schemas/negotiation-digest.schema.d.ts +2 -2
- package/package.json +2 -15
- package/dist/contacts/application/contact.inviter.d.ts +0 -49
- package/dist/contacts/application/contact.inviter.js +0 -64
- package/dist/integrations/application/index.d.ts +0 -14
- package/dist/integrations/application/index.js +0 -14
- package/dist/integrations/application/integration.tools.d.ts +0 -27
- package/dist/integrations/application/integration.tools.js +0 -98
- package/dist/integrations/domain/index.d.ts +0 -16
- package/dist/integrations/domain/index.js +0 -1
- package/dist/integrations/domain/integration.types.d.ts +0 -51
- package/dist/integrations/domain/integration.types.js +0 -9
- package/dist/integrations/index.d.ts +0 -10
- package/dist/integrations/index.js +0 -8
- package/dist/integrations/ports/index.d.ts +0 -25
- package/dist/integrations/ports/index.js +0 -23
- package/dist/integrations/ports/integration.adapter.port.d.ts +0 -62
- package/dist/integrations/ports/integration.adapter.port.js +0 -15
- package/dist/integrations/ports/integration.importer.port.d.ts +0 -31
- package/dist/integrations/ports/integration.importer.port.js +0 -15
- package/dist/integrations/ports/integration.tools.port.d.ts +0 -25
- package/dist/integrations/ports/integration.tools.port.js +0 -18
- package/dist/opportunities/application/opportunity.card-presentation.d.ts +0 -44
- package/dist/opportunities/application/opportunity.card-presentation.js +0 -93
- package/dist/opportunities/application/opportunity.presenter.d.ts +0 -156
- package/dist/opportunities/domain/opportunity.presentation-cache.d.ts +0 -5
- package/dist/opportunities/domain/opportunity.presentation-cache.js +0 -11
- package/dist/opportunities/domain/opportunity.presentation.d.ts +0 -76
- package/dist/opportunities/domain/opportunity.presentation.js +0 -516
- package/dist/opportunities/domain/opportunity.safe-presentation.d.ts +0 -104
- package/dist/opportunities/domain/opportunity.safe-presentation.js +0 -102
|
@@ -1,720 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The opportunity tool registry: list, update, and delivery confirmation.
|
|
3
|
+
*
|
|
4
|
+
* `list_opportunities` lives in `opportunity.tools.list.ts` and the shared card
|
|
5
|
+
* and link helpers in `opportunity.tools.cards.ts`; this file wires the three
|
|
6
|
+
* tools together and owns the two mutation tools.
|
|
7
|
+
*/
|
|
1
8
|
import { z } from "zod";
|
|
2
9
|
import { requestContext } from "../../shared/observability/request-context.js";
|
|
3
10
|
import { success, error, UUID_REGEX } from "../../shared/agent/tool.helpers.js";
|
|
4
|
-
import { focusedIntentId, focusedNetworkId
|
|
5
|
-
import {
|
|
6
|
-
import { narratorRemarkFromReasoning, stripUuids } from "../domain/opportunity.presentation.js";
|
|
7
|
-
import { safeFallbackSummary } from "../domain/opportunity.safe-presentation.js";
|
|
8
|
-
import { buildOpportunityPresentation } from "./opportunity.card-presentation.js";
|
|
11
|
+
import { focusedIntentId, focusedNetworkId } from "../../shared/agent/tool.scope.js";
|
|
12
|
+
import { OpportunityPresenter, gatherPresenterContext } from "./opportunity.presentation.js";
|
|
9
13
|
import { isUptakeGuardEnabled } from "../../questions/index.js";
|
|
10
|
-
import { OpportunityPresenter, gatherPresenterContext } from "./opportunity.presenter.js";
|
|
11
|
-
import { loadNegotiationContext } from "./negotiation-context.loader.js";
|
|
12
14
|
import { admitOpportunityUpdate } from './opportunity.update-admission.js';
|
|
13
15
|
import { opportunityOwnerActionForStatus } from './opportunity.owner-approval.js';
|
|
14
16
|
import { ownerApprovalProvenanceFor } from './opportunity.owner-provenance.js';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return remark;
|
|
21
|
-
const name = narratorName.trim();
|
|
22
|
-
const nameLower = name.toLowerCase();
|
|
23
|
-
for (;;) {
|
|
24
|
-
const lower = t.toLowerCase();
|
|
25
|
-
if (!lower.startsWith(nameLower))
|
|
26
|
-
break;
|
|
27
|
-
// Require a word boundary after the name so a short name like "Al" does not
|
|
28
|
-
// mangle a longer word ("Always …" → "ways …"). The char after the name must
|
|
29
|
-
// be a separator (sentence/clause punctuation or whitespace) or end of string.
|
|
30
|
-
// Keep this set in sync with the separator stripped from `rest` below.
|
|
31
|
-
const boundary = t.charAt(name.length);
|
|
32
|
-
if (boundary && !/[\s.:,\-–—]/.test(boundary))
|
|
33
|
-
break;
|
|
34
|
-
const rest = t.slice(name.length).replace(/^\s*[.:,\-–—]\s*/i, '').trim();
|
|
35
|
-
if (rest.length === 0 || rest === t)
|
|
36
|
-
break;
|
|
37
|
-
t = rest;
|
|
38
|
-
}
|
|
39
|
-
return t;
|
|
40
|
-
}
|
|
41
|
-
import { protocolLogger } from "../../shared/observability/protocol.logger.js";
|
|
42
|
-
import { invokeWithAbortSignal } from "../../shared/agent/model-signal.js";
|
|
43
|
-
const logger = protocolLogger("ChatTools:Opportunity");
|
|
44
|
-
/**
|
|
45
|
-
* Build the agent-facing profile link for a counterpart — always the Index web
|
|
46
|
-
* profile URL with `?link_preview=false`. Returns `undefined` only if
|
|
47
|
-
* `frontendUrl` is not configured.
|
|
48
|
-
*
|
|
49
|
-
* The `?link_preview=false` hint is honored by chat-gateway runtimes (e.g.
|
|
50
|
-
* OpenClaw's Telegram delivery) that strip link previews when present in the
|
|
51
|
-
* URL; consistent placement matters more than Telegram's own handling.
|
|
52
|
-
*
|
|
53
|
-
* Trailing slashes on frontendUrl are stripped before concatenation.
|
|
54
|
-
*/
|
|
55
|
-
export function buildProfileUrl(counterpartUserId, frontendUrl) {
|
|
56
|
-
// The profile link is always the Index web profile, regardless of the
|
|
57
|
-
// counterpart's socials.
|
|
58
|
-
if (!frontendUrl)
|
|
59
|
-
return undefined;
|
|
60
|
-
const base = frontendUrl.replace(/\/+$/, "");
|
|
61
|
-
return `${base}/u/${counterpartUserId}?link_preview=false`;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Build the deep-link to an opportunity's A2A negotiation trace
|
|
65
|
-
* (`/chat/:conversationId`) so users can see *what negotiation led to* the
|
|
66
|
-
* surfaced opportunity (EDG-50/EDG-51). Returns `undefined` when `frontendUrl`
|
|
67
|
-
* is unset or there is no negotiation conversation to link to.
|
|
68
|
-
*
|
|
69
|
-
* The `?link_preview=false` hint mirrors `buildProfileUrl` — chat-gateway
|
|
70
|
-
* runtimes (e.g. Telegram delivery) strip link previews when it is present.
|
|
71
|
-
* Trailing slashes on `frontendUrl` are stripped before concatenation.
|
|
72
|
-
*/
|
|
73
|
-
export function buildNegotiationUrl(conversationId, frontendUrl) {
|
|
74
|
-
if (!frontendUrl || !conversationId)
|
|
75
|
-
return undefined;
|
|
76
|
-
const base = frontendUrl.replace(/\/+$/, "");
|
|
77
|
-
return `${base}/chat/${conversationId}?link_preview=false`;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Build the agent-facing deep link for an opportunity — the canonical
|
|
81
|
-
* `https://index.network/o/<id>` universal link. Returns `undefined` when
|
|
82
|
-
* `frontendUrl` is unset or there is no opportunity id.
|
|
83
|
-
*
|
|
84
|
-
* This is the *only* place the protocol mints an opportunity deep link. It is
|
|
85
|
-
* a navigation link, not an authority: opening it raises the opportunity card
|
|
86
|
-
* in the Index macOS app when installed and a static Index landing page
|
|
87
|
-
* otherwise. Acceptance stays an authenticated call.
|
|
88
|
-
*
|
|
89
|
-
* No `?link_preview=false` hint here (unlike `buildProfileUrl`): the Hermes
|
|
90
|
-
* plugin mints the identical bare form for payloads the protocol does not
|
|
91
|
-
* touch, and keeping the two byte-identical is what makes its never-overwrite
|
|
92
|
-
* rule invisible.
|
|
93
|
-
*/
|
|
94
|
-
export function buildOpportunityAppUrl(opportunityId, frontendUrl) {
|
|
95
|
-
if (!frontendUrl || !opportunityId)
|
|
96
|
-
return undefined;
|
|
97
|
-
const base = frontendUrl.replace(/\/+$/, "");
|
|
98
|
-
return `${base}/o/${opportunityId}`;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Attach the agent-facing profile link for a counterpart to `card` (mutates
|
|
102
|
-
* in place). Every counterpart has a profile page worth linking to — without
|
|
103
|
-
* this, the agent gets a name with no URL attached and tends to fabricate
|
|
104
|
-
* one. Accept/act guidance is plain text ("accept in the Index app"); no
|
|
105
|
-
* actionable URLs are minted here.
|
|
106
|
-
*/
|
|
107
|
-
export function attachProfileLink(card, opts) {
|
|
108
|
-
const profileUrl = buildProfileUrl(opts.counterpartUserId, opts.frontendUrl);
|
|
109
|
-
if (profileUrl)
|
|
110
|
-
card.profileUrl = profileUrl;
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Attach the opportunity deep link to `card` (mutates in place) so every MCP
|
|
114
|
-
* client — Claude Desktop, the CLI, the web, Hermes — can hand the user one
|
|
115
|
-
* clickable link to the card instead of fabricating one from an id.
|
|
116
|
-
*/
|
|
117
|
-
export function attachOpportunityAppLink(card, opts) {
|
|
118
|
-
const appUrl = buildOpportunityAppUrl(card.opportunityId, opts.frontendUrl);
|
|
119
|
-
if (appUrl)
|
|
120
|
-
card.appUrl = appUrl;
|
|
121
|
-
}
|
|
122
|
-
function publicUptakeQuestion(question) {
|
|
123
|
-
return {
|
|
124
|
-
id: question.id,
|
|
125
|
-
title: question.title,
|
|
126
|
-
prompt: question.prompt,
|
|
127
|
-
options: question.options,
|
|
128
|
-
multiSelect: question.multiSelect,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
function uptakeAdvisory(opportunityId, questions) {
|
|
132
|
-
return JSON.stringify({
|
|
133
|
-
success: false,
|
|
134
|
-
error: "Resolve the pending uptake questions or explicitly continue anyway.",
|
|
135
|
-
advisory: {
|
|
136
|
-
code: "unresolved_uptake_questions",
|
|
137
|
-
advisoryOnly: true,
|
|
138
|
-
opportunityId,
|
|
139
|
-
questions,
|
|
140
|
-
acknowledgedUptakeQuestionIds: questions.map((question) => question.id),
|
|
141
|
-
},
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* IND-593: stable fail-closed denial for the owner-approval boundary. The
|
|
146
|
-
* `missing` reason carries the fresh, server-derived interaction challenge the
|
|
147
|
-
* owner must explicitly approve; all other reasons carry no challenge.
|
|
148
|
-
*/
|
|
149
|
-
function ownerApprovalDenial(opportunityId, action, verdict) {
|
|
150
|
-
return JSON.stringify({
|
|
151
|
-
success: false,
|
|
152
|
-
error: `Owner approval required for this opportunity ${action} (${verdict.reason}).`,
|
|
153
|
-
approval: {
|
|
154
|
-
code: "owner_approval_required",
|
|
155
|
-
reason: verdict.reason,
|
|
156
|
-
opportunityId,
|
|
157
|
-
action,
|
|
158
|
-
...(verdict.challenge
|
|
159
|
-
? { interactionId: verdict.challenge.interactionId, expiresAt: verdict.challenge.expiresAt }
|
|
160
|
-
: {}),
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Maximum number of opportunity cards to show per chat response.
|
|
166
|
-
* Sized for `selectByComposition` to fill both feed buckets — up to 3
|
|
167
|
-
* connection + 3 connector-flow per the digest/ambient prompt rules.
|
|
168
|
-
*/
|
|
169
|
-
const CHAT_DISPLAY_LIMIT = 6;
|
|
170
|
-
/**
|
|
171
|
-
* Build minimal opportunity card data for chat without calling the LLM presenter.
|
|
172
|
-
* Uses only required fields from the opportunity record and counterpart name/avatar
|
|
173
|
-
* so list_opportunities and discovery return quickly.
|
|
174
|
-
*
|
|
175
|
-
* Note: narratorChip.text is generated via regex heuristics (narratorRemarkFromReasoning)
|
|
176
|
-
* rather than the OpportunityPresenter LLM. If narrator quality becomes an issue again,
|
|
177
|
-
* consider making this function async and delegating to OpportunityPresenter.presentCard()
|
|
178
|
-
* which already produces a high-quality narratorRemark via LLM (used by the home graph
|
|
179
|
-
* and discovery pipeline). The trade-off is 5-20s latency per card.
|
|
180
|
-
*
|
|
181
|
-
* Exported for use in tests (opportunity.tools.spec.ts).
|
|
182
|
-
*/
|
|
183
|
-
export function buildMinimalOpportunityCard(opp, viewerId, counterpartUserId, counterpartName, counterpartAvatar, introducerName, introducerAvatar, viewerName, secondPartyName, secondPartyAvatar, secondPartyUserId, isCounterpartGhost) {
|
|
184
|
-
const viewerActor = opp.actors.find((a) => a.userId === viewerId);
|
|
185
|
-
const viewerRole = viewerActor?.role ?? "party";
|
|
186
|
-
const introducerActor = opp.actors.find((a) => a.role === "introducer" && a.userId !== viewerId);
|
|
187
|
-
const viewerIsIntroducer = opp.actors.some((a) => a.role === "introducer" && a.userId === viewerId);
|
|
188
|
-
const reasoning = opp.interpretation?.reasoning ?? "";
|
|
189
|
-
// Shared sanitization standard — see opportunity.safe-presentation.ts.
|
|
190
|
-
const mainText = safeFallbackSummary(reasoning, {
|
|
191
|
-
counterpartName,
|
|
192
|
-
viewerName,
|
|
193
|
-
introducerName: introducerName ?? undefined,
|
|
194
|
-
maxChars: MINIMAL_MAIN_TEXT_MAX_CHARS,
|
|
195
|
-
emptyText: "A suggested connection.",
|
|
196
|
-
});
|
|
197
|
-
const score = typeof opp.interpretation?.confidence === "number"
|
|
198
|
-
? opp.interpretation.confidence
|
|
199
|
-
: undefined;
|
|
200
|
-
const narratorName = viewerIsIntroducer
|
|
201
|
-
? "You"
|
|
202
|
-
: introducerName?.trim() || (introducerActor ? "Someone" : "Index");
|
|
203
|
-
const primaryActionLabel = getPrimaryActionLabel(viewerRole);
|
|
204
|
-
return {
|
|
205
|
-
opportunityId: opp.id,
|
|
206
|
-
userId: counterpartUserId,
|
|
207
|
-
name: counterpartName,
|
|
208
|
-
avatar: counterpartAvatar,
|
|
209
|
-
mainText,
|
|
210
|
-
cta: "Start a conversation to connect.",
|
|
211
|
-
headline: viewerIsIntroducer && secondPartyName
|
|
212
|
-
? `${counterpartName} → ${secondPartyName}`
|
|
213
|
-
: `Connection with ${counterpartName}`,
|
|
214
|
-
primaryActionLabel,
|
|
215
|
-
secondaryActionLabel: SECONDARY_ACTION_LABEL,
|
|
216
|
-
mutualIntentsLabel: "Suggested connection",
|
|
217
|
-
narratorChip: {
|
|
218
|
-
name: narratorName,
|
|
219
|
-
text: narratorRemarkFromReasoning(reasoning, counterpartName, viewerName),
|
|
220
|
-
...(viewerIsIntroducer
|
|
221
|
-
? { userId: viewerId, avatar: null }
|
|
222
|
-
: introducerActor
|
|
223
|
-
? { userId: introducerActor.userId, avatar: introducerAvatar ?? null }
|
|
224
|
-
: {}),
|
|
225
|
-
},
|
|
226
|
-
viewerRole,
|
|
227
|
-
score,
|
|
228
|
-
status: opp.status ?? "latent",
|
|
229
|
-
isGhost: isCounterpartGhost ?? false,
|
|
230
|
-
...(viewerIsIntroducer && secondPartyName
|
|
231
|
-
? {
|
|
232
|
-
secondParty: {
|
|
233
|
-
name: secondPartyName,
|
|
234
|
-
...(secondPartyAvatar != null ? { avatar: secondPartyAvatar } : {}),
|
|
235
|
-
...(secondPartyUserId ? { userId: secondPartyUserId } : {}),
|
|
236
|
-
},
|
|
237
|
-
}
|
|
238
|
-
: {}),
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
function confirmDeliveryError(code, retryable, message) {
|
|
242
|
-
return JSON.stringify({ success: false, error: message, code, retryable });
|
|
243
|
-
}
|
|
17
|
+
export { buildOpportunityPresentation } from "./opportunity.presentation.js";
|
|
18
|
+
import { sendOpportunity, updateOpportunityStatus } from "./opportunity.graph.modes.js";
|
|
19
|
+
import { createListOpportunitiesTool } from "./opportunity.tools.list.js";
|
|
20
|
+
import { confirmDeliveryError, logger, ownerApprovalDenial, publicUptakeQuestion, uptakeAdvisory } from "./opportunity.tools.cards.js";
|
|
21
|
+
export { attachOpportunityAppLink, attachProfileLink, buildMinimalOpportunityCard, buildNegotiationUrl, buildOpportunityAppUrl, buildProfileUrl } from "./opportunity.tools.cards.js";
|
|
244
22
|
export function createOpportunityTools(defineTool, deps) {
|
|
245
23
|
const { database, userDb, systemDb, graphs, cache } = deps;
|
|
246
24
|
const createOpportunityPresenter = deps.opportunityPresentation?.createPresenter ??
|
|
247
25
|
(() => new OpportunityPresenter());
|
|
248
26
|
const gatherOpportunityPresenterContext = deps.opportunityPresentation?.gatherPresenterContext ??
|
|
249
27
|
gatherPresenterContext;
|
|
250
|
-
const listOpportunities = defineTool
|
|
251
|
-
name: "list_opportunities",
|
|
252
|
-
description: "Lists the authenticated user's actionable opportunities (discovered connections). Returns opportunity cards ready for display.\n\n" +
|
|
253
|
-
"**What are opportunities?** Matches between users whose intents complement each other within shared networkes. " +
|
|
254
|
-
"Each opportunity has a status: draft (not yet sent), pending (sent, awaiting response), accepted, rejected, or expired.\n\n" +
|
|
255
|
-
"**What this returns:** Only draft and pending opportunities — the ones the user can still act on. " +
|
|
256
|
-
"Accepted, rejected, and expired ones are not surfaced through this tool.\n\n" +
|
|
257
|
-
"**When to use:** When the user wants to see their current matches or review what's waiting for their response.\n\n" +
|
|
258
|
-
"**Returns:** Up to 3 opportunity code blocks (interactive cards) with counterpart name, match reasoning, confidence score, " +
|
|
259
|
-
"and current status. Use update_opportunity to act on them (send, accept, reject).",
|
|
260
|
-
querySchema: z.object({
|
|
261
|
-
networkId: z
|
|
262
|
-
.string()
|
|
263
|
-
.optional()
|
|
264
|
-
.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."),
|
|
265
|
-
scopeType: z
|
|
266
|
-
.enum(['intent'])
|
|
267
|
-
.optional()
|
|
268
|
-
.describe("Optional selected scope type. Use 'intent' to narrow listed opportunities to a selected intent."),
|
|
269
|
-
scopeId: z
|
|
270
|
-
.string()
|
|
271
|
-
.optional()
|
|
272
|
-
.describe("Selected intent UUID when scopeType is 'intent'. Ignored only when absent."),
|
|
273
|
-
includeDigestMarkers: z
|
|
274
|
-
.boolean()
|
|
275
|
-
.optional()
|
|
276
|
-
.describe("Internal scheduled-digest mode only. When true, includes hidden delivery markers so the digest send pass can confirm only edited-in opportunities."),
|
|
277
|
-
}),
|
|
278
|
-
handler: async ({ context, query }) => {
|
|
279
|
-
const scopedNetworkId = focusedNetworkId(context) ?? context.networkId?.trim();
|
|
280
|
-
const scopedIndexLabel = focusedNetworkLabel(context);
|
|
281
|
-
// Strict scope enforcement: when chat is network-scoped, only allow that index
|
|
282
|
-
if (scopedNetworkId &&
|
|
283
|
-
query.networkId?.trim() &&
|
|
284
|
-
query.networkId.trim() !== scopedNetworkId) {
|
|
285
|
-
return error(`This chat is scoped to ${scopedIndexLabel}. You can only list opportunities from this community.`);
|
|
286
|
-
}
|
|
287
|
-
const effectiveIndexId = (scopedNetworkId || query.networkId?.trim()) ?? undefined;
|
|
288
|
-
if (effectiveIndexId && !UUID_REGEX.test(effectiveIndexId)) {
|
|
289
|
-
return error("Invalid network ID format.");
|
|
290
|
-
}
|
|
291
|
-
const contextIntentId = focusedIntentId(context);
|
|
292
|
-
const rawScopeId = query.scopeId?.trim() || undefined;
|
|
293
|
-
if (query.scopeType === 'intent' && !rawScopeId) {
|
|
294
|
-
return error("scopeId required when scopeType is intent.");
|
|
295
|
-
}
|
|
296
|
-
if (!query.scopeType && rawScopeId) {
|
|
297
|
-
return error("scopeType=intent required when scopeId is provided.");
|
|
298
|
-
}
|
|
299
|
-
if (rawScopeId && !UUID_REGEX.test(rawScopeId)) {
|
|
300
|
-
return error("Invalid scope ID format.");
|
|
301
|
-
}
|
|
302
|
-
if (contextIntentId && rawScopeId && contextIntentId !== rawScopeId) {
|
|
303
|
-
return error("This chat is scoped to a different intent.");
|
|
304
|
-
}
|
|
305
|
-
const effectiveIntentScope = contextIntentId
|
|
306
|
-
? { scopeType: 'intent', scopeId: contextIntentId }
|
|
307
|
-
: query.scopeType === 'intent' && rawScopeId
|
|
308
|
-
? { scopeType: 'intent', scopeId: rawScopeId }
|
|
309
|
-
: {};
|
|
310
|
-
const selection = await selectOpportunityFeed({
|
|
311
|
-
reader: database,
|
|
312
|
-
deliveryLedger: deps.deliveryLedger,
|
|
313
|
-
viewerId: context.userId,
|
|
314
|
-
networkId: effectiveIndexId,
|
|
315
|
-
intentScope: effectiveIntentScope,
|
|
316
|
-
isMcp: context.isMcp === true,
|
|
317
|
-
includeDigestMarkers: query.includeDigestMarkers,
|
|
318
|
-
displayLimit: CHAT_DISPLAY_LIMIT,
|
|
319
|
-
warn: (message, data) => logger.warn(message, data),
|
|
320
|
-
});
|
|
321
|
-
const { opportunities, dedupedCount, skippedIds, redeliveryIds, fetchedCount, isDigestMode } = selection;
|
|
322
|
-
const buildListDebugSteps = () => {
|
|
323
|
-
const steps = [];
|
|
324
|
-
if (skippedIds.length > 0) {
|
|
325
|
-
steps.push({
|
|
326
|
-
step: "opportunity_display_skips",
|
|
327
|
-
detail: `${skippedIds.length} opportunity card(s) couldn't be displayed`,
|
|
328
|
-
data: {
|
|
329
|
-
skippedCount: skippedIds.length,
|
|
330
|
-
totalOpportunities: fetchedCount,
|
|
331
|
-
skippedOpportunityIds: skippedIds,
|
|
332
|
-
},
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
return steps;
|
|
336
|
-
};
|
|
337
|
-
if (!opportunities || opportunities.length === 0) {
|
|
338
|
-
if (skippedIds.length > 0) {
|
|
339
|
-
const listDebugSteps = buildListDebugSteps();
|
|
340
|
-
return success({
|
|
341
|
-
found: false,
|
|
342
|
-
count: 0,
|
|
343
|
-
summary: "Some opportunities couldn't be displayed",
|
|
344
|
-
message: "I found opportunities, but couldn't render them. Please try again.",
|
|
345
|
-
...(listDebugSteps.length ? { debugSteps: listDebugSteps } : {}),
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
// Digest mode: distinguish "everything was already shown" from "nothing
|
|
349
|
-
// exists" so the brief omits the people section instead of prompting
|
|
350
|
-
// the user to run discovery.
|
|
351
|
-
if (isDigestMode && dedupedCount > 0) {
|
|
352
|
-
return success({
|
|
353
|
-
found: false,
|
|
354
|
-
count: 0,
|
|
355
|
-
summary: "No new opportunities to show",
|
|
356
|
-
message: "No new opportunities today — everything actionable has already been shown recently. Omit the people section from the digest.",
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
return success({
|
|
360
|
-
found: false,
|
|
361
|
-
count: 0,
|
|
362
|
-
summary: "No opportunities yet",
|
|
363
|
-
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.",
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
// Batch-fetch profiles and users for all counterpart and introducer userIds to avoid N+1
|
|
367
|
-
const counterpartUserIds = new Set();
|
|
368
|
-
const introducerUserIds = new Set();
|
|
369
|
-
for (const opp of opportunities) {
|
|
370
|
-
const counterpartActor = opp.actors.find((a) => a.userId !== context.userId && a.role !== "introducer");
|
|
371
|
-
if (counterpartActor?.userId)
|
|
372
|
-
counterpartUserIds.add(counterpartActor.userId);
|
|
373
|
-
const introducerActor = opp.actors.find((a) => a.role === "introducer" && a.userId !== context.userId);
|
|
374
|
-
if (introducerActor?.userId)
|
|
375
|
-
introducerUserIds.add(introducerActor.userId);
|
|
376
|
-
}
|
|
377
|
-
const allUserIds = [
|
|
378
|
-
...new Set([...counterpartUserIds, ...introducerUserIds]),
|
|
379
|
-
];
|
|
380
|
-
const [profileResults, userResults] = await Promise.all([
|
|
381
|
-
Promise.all(allUserIds.map((id) => database.getProfile(id))),
|
|
382
|
-
Promise.all(allUserIds.map((id) => database.getUser(id))),
|
|
383
|
-
]);
|
|
384
|
-
const profileMap = new Map();
|
|
385
|
-
const userMap = new Map();
|
|
386
|
-
allUserIds.forEach((userId, i) => {
|
|
387
|
-
const profile = profileResults[i] ?? null;
|
|
388
|
-
const user = userResults[i] ?? null;
|
|
389
|
-
if (profile)
|
|
390
|
-
profileMap.set(userId, profile);
|
|
391
|
-
if (user)
|
|
392
|
-
userMap.set(userId, user);
|
|
393
|
-
});
|
|
394
|
-
const cardDataList = [];
|
|
395
|
-
const seenOpportunityIds = new Set();
|
|
396
|
-
if (isDigestMode) {
|
|
397
|
-
// ── Digest mode: use LLM presenter for rich, second-person card text ──
|
|
398
|
-
const presenter = createOpportunityPresenter();
|
|
399
|
-
const presenterDb = database;
|
|
400
|
-
const PRESENTER_CONCURRENCY = 6;
|
|
401
|
-
for (let i = 0; i < opportunities.length; i += PRESENTER_CONCURRENCY) {
|
|
402
|
-
const chunk = opportunities.slice(i, i + PRESENTER_CONCURRENCY);
|
|
403
|
-
const chunkCards = await Promise.all(chunk.map(async (opp) => {
|
|
404
|
-
if (seenOpportunityIds.has(opp.id))
|
|
405
|
-
return null;
|
|
406
|
-
seenOpportunityIds.add(opp.id);
|
|
407
|
-
try {
|
|
408
|
-
const counterpartActor = opp.actors.find((a) => a.userId !== context.userId && a.role !== "introducer");
|
|
409
|
-
const counterpartUserId = counterpartActor?.userId;
|
|
410
|
-
if (!counterpartUserId)
|
|
411
|
-
return null;
|
|
412
|
-
const viewerIsIntroducerHere = opp.actors.some((a) => a.role === "introducer" && a.userId === context.userId);
|
|
413
|
-
const secondPartyActorForHeadline = viewerIsIntroducerHere
|
|
414
|
-
? opp.actors.find((a) => a.userId !== context.userId &&
|
|
415
|
-
a.userId !== counterpartUserId &&
|
|
416
|
-
a.role !== "introducer")
|
|
417
|
-
: undefined;
|
|
418
|
-
const introducerActor = opp.actors.find((a) => a.role === "introducer" && a.userId !== context.userId);
|
|
419
|
-
const createdByName = opp.detection.createdByName;
|
|
420
|
-
const counterpartUser = userMap.get(counterpartUserId) ?? null;
|
|
421
|
-
const counterpartName = profileMap.get(counterpartUserId)?.identity?.name ??
|
|
422
|
-
counterpartUser?.name ??
|
|
423
|
-
"Someone";
|
|
424
|
-
const introducerName = createdByName ??
|
|
425
|
-
(introducerActor
|
|
426
|
-
? (profileMap.get(introducerActor.userId)?.identity?.name ?? null)
|
|
427
|
-
: null);
|
|
428
|
-
const introducerUser = introducerActor
|
|
429
|
-
? userMap.get(introducerActor.userId) ?? null
|
|
430
|
-
: null;
|
|
431
|
-
const secondPartyUser = secondPartyActorForHeadline
|
|
432
|
-
? userMap.get(secondPartyActorForHeadline.userId) ?? null
|
|
433
|
-
: null;
|
|
434
|
-
const secondPartyNameForHeadline = secondPartyActorForHeadline
|
|
435
|
-
? (profileMap.get(secondPartyActorForHeadline.userId)?.identity?.name ??
|
|
436
|
-
secondPartyUser?.name ??
|
|
437
|
-
undefined)
|
|
438
|
-
: undefined;
|
|
439
|
-
const viewerActor = opp.actors.find((a) => a.userId === context.userId);
|
|
440
|
-
const viewerRole = viewerActor?.role ?? "party";
|
|
441
|
-
const isCounterpartGhost = counterpartUser?.isGhost ?? false;
|
|
442
|
-
try {
|
|
443
|
-
// Load the negotiation context alongside presenter context so
|
|
444
|
-
// the digest copy can explain *why* the opportunity surfaced
|
|
445
|
-
// (EDG-50) — the presenter grounds `digestSummary` in concrete
|
|
446
|
-
// negotiation turns when this is present. The same context
|
|
447
|
-
// yields the conversationId for the negotiation-trace link
|
|
448
|
-
// (EDG-51).
|
|
449
|
-
const [ctx, negotiationContext] = await Promise.all([
|
|
450
|
-
gatherOpportunityPresenterContext(presenterDb, opp, context.userId, counterpartUserId),
|
|
451
|
-
loadNegotiationContext(deps.negotiationDatabase, opp.id, opp.status),
|
|
452
|
-
]);
|
|
453
|
-
const presentation = await presenter.presentCard({
|
|
454
|
-
...ctx,
|
|
455
|
-
opportunityStatus: opp.status,
|
|
456
|
-
...(negotiationContext ? { negotiationContext } : {}),
|
|
457
|
-
});
|
|
458
|
-
const negotiationUrl = buildNegotiationUrl(negotiationContext?.conversationId, deps.frontendUrl);
|
|
459
|
-
// Build narrator chip from presenter output
|
|
460
|
-
let narratorChip;
|
|
461
|
-
const introducerIsCounterpart = introducerActor && counterpartActor && introducerActor.userId === counterpartActor.userId;
|
|
462
|
-
if (introducerActor && introducerActor.userId !== context.userId && !introducerIsCounterpart) {
|
|
463
|
-
const narratorName = introducerName?.trim() || "Someone";
|
|
464
|
-
narratorChip = {
|
|
465
|
-
name: narratorName,
|
|
466
|
-
text: stripLeadingNarratorName(presentation.narratorRemark, narratorName),
|
|
467
|
-
avatar: introducerUser?.avatar ?? null,
|
|
468
|
-
userId: introducerActor.userId,
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
else if (introducerActor?.userId === context.userId) {
|
|
472
|
-
narratorChip = { name: "You", text: presentation.narratorRemark, userId: context.userId };
|
|
473
|
-
}
|
|
474
|
-
else {
|
|
475
|
-
narratorChip = { name: "Index", text: presentation.narratorRemark };
|
|
476
|
-
}
|
|
477
|
-
const card = {
|
|
478
|
-
opportunityId: opp.id,
|
|
479
|
-
userId: counterpartUserId,
|
|
480
|
-
name: counterpartName,
|
|
481
|
-
avatar: counterpartUser?.avatar ?? null,
|
|
482
|
-
mainText: stripUuids(presentation.personalizedSummary),
|
|
483
|
-
digestSummary: stripUuids(presentation.digestSummary),
|
|
484
|
-
// Deep-link to the negotiation trace that produced this card
|
|
485
|
-
// (EDG-51). Only present when a negotiation conversation exists.
|
|
486
|
-
...(negotiationUrl ? { negotiationUrl } : {}),
|
|
487
|
-
cta: presentation.suggestedAction,
|
|
488
|
-
headline: viewerIsIntroducerHere && secondPartyNameForHeadline
|
|
489
|
-
? `${counterpartName} → ${secondPartyNameForHeadline}`
|
|
490
|
-
: presentation.headline,
|
|
491
|
-
primaryActionLabel: getPrimaryActionLabel(viewerRole),
|
|
492
|
-
secondaryActionLabel: SECONDARY_ACTION_LABEL,
|
|
493
|
-
mutualIntentsLabel: presentation.mutualIntentsLabel,
|
|
494
|
-
narratorChip,
|
|
495
|
-
viewerRole,
|
|
496
|
-
score: typeof opp.interpretation?.confidence === "number"
|
|
497
|
-
? opp.interpretation.confidence
|
|
498
|
-
: undefined,
|
|
499
|
-
status: opp.status,
|
|
500
|
-
isGhost: isCounterpartGhost,
|
|
501
|
-
...(redeliveryIds.has(opp.id) ? { redelivery: true } : {}),
|
|
502
|
-
...(viewerIsIntroducerHere && secondPartyNameForHeadline
|
|
503
|
-
? {
|
|
504
|
-
secondParty: {
|
|
505
|
-
name: secondPartyNameForHeadline,
|
|
506
|
-
...(secondPartyUser?.avatar != null ? { avatar: secondPartyUser.avatar } : {}),
|
|
507
|
-
...(secondPartyActorForHeadline?.userId ? { userId: secondPartyActorForHeadline.userId } : {}),
|
|
508
|
-
},
|
|
509
|
-
}
|
|
510
|
-
: {}),
|
|
511
|
-
};
|
|
512
|
-
// Attach the agent-facing profile and opportunity links for
|
|
513
|
-
// MCP callers
|
|
514
|
-
if (context.isMcp) {
|
|
515
|
-
attachProfileLink(card, {
|
|
516
|
-
counterpartUserId,
|
|
517
|
-
frontendUrl: deps.frontendUrl,
|
|
518
|
-
});
|
|
519
|
-
attachOpportunityAppLink(card, {
|
|
520
|
-
frontendUrl: deps.frontendUrl,
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
return card;
|
|
524
|
-
}
|
|
525
|
-
catch (presenterErr) {
|
|
526
|
-
logger.warn("LLM presenter failed for list_opportunities digest card, skipping raw fallback", {
|
|
527
|
-
opportunityId: opp.id,
|
|
528
|
-
err: presenterErr,
|
|
529
|
-
});
|
|
530
|
-
// Scheduled digests should only surface OpportunityPresenter-rendered
|
|
531
|
-
// copy. The minimal fallback reuses evaluator reasoning, which can
|
|
532
|
-
// contain raw narrator phrasing (for example "The discoverer...")
|
|
533
|
-
// and is not suitable for AgentVillage morning briefs.
|
|
534
|
-
skippedIds.push(opp.id);
|
|
535
|
-
return null;
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
catch (err) {
|
|
539
|
-
logger.warn("Skipping opportunity that failed to build card", {
|
|
540
|
-
opportunityId: opp.id,
|
|
541
|
-
err,
|
|
542
|
-
});
|
|
543
|
-
skippedIds.push(opp.id);
|
|
544
|
-
return null;
|
|
545
|
-
}
|
|
546
|
-
}));
|
|
547
|
-
for (const card of chunkCards) {
|
|
548
|
-
if (card)
|
|
549
|
-
cardDataList.push(card);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
else {
|
|
554
|
-
// ── Chat/list mode: use OpportunityPresenter for user-facing card copy ──
|
|
555
|
-
const presenter = createOpportunityPresenter();
|
|
556
|
-
const presenterDb = database;
|
|
557
|
-
const PRESENTER_CONCURRENCY = 6;
|
|
558
|
-
for (let i = 0; i < opportunities.length; i += PRESENTER_CONCURRENCY) {
|
|
559
|
-
const chunk = opportunities.slice(i, i + PRESENTER_CONCURRENCY);
|
|
560
|
-
const chunkCards = await Promise.all(chunk.map(async (opp) => {
|
|
561
|
-
if (seenOpportunityIds.has(opp.id))
|
|
562
|
-
return null;
|
|
563
|
-
seenOpportunityIds.add(opp.id);
|
|
564
|
-
try {
|
|
565
|
-
const counterpartActor = opp.actors.find((a) => a.userId !== context.userId && a.role !== "introducer");
|
|
566
|
-
const counterpartUserId = counterpartActor?.userId;
|
|
567
|
-
if (!counterpartUserId)
|
|
568
|
-
return null;
|
|
569
|
-
const viewerIsIntroducerHere = opp.actors.some((a) => a.role === "introducer" && a.userId === context.userId);
|
|
570
|
-
const secondPartyActorForHeadline = viewerIsIntroducerHere
|
|
571
|
-
? opp.actors.find((a) => a.userId !== context.userId &&
|
|
572
|
-
a.userId !== counterpartUserId &&
|
|
573
|
-
a.role !== "introducer")
|
|
574
|
-
: undefined;
|
|
575
|
-
const introducerActor = opp.actors.find((a) => a.role === "introducer" && a.userId !== context.userId);
|
|
576
|
-
const createdByName = opp.detection.createdByName;
|
|
577
|
-
const counterpartProfile = profileMap.get(counterpartUserId) ?? null;
|
|
578
|
-
const counterpartUser = userMap.get(counterpartUserId) ?? null;
|
|
579
|
-
const introducerProfile = introducerActor && !createdByName
|
|
580
|
-
? profileMap.get(introducerActor.userId) ?? null
|
|
581
|
-
: null;
|
|
582
|
-
const counterpartName = counterpartProfile?.identity?.name ??
|
|
583
|
-
counterpartUser?.name ??
|
|
584
|
-
"Someone";
|
|
585
|
-
const introducerName = createdByName ??
|
|
586
|
-
(introducerActor ? introducerProfile?.identity?.name ?? null : null);
|
|
587
|
-
const introducerUser = introducerActor
|
|
588
|
-
? userMap.get(introducerActor.userId) ?? null
|
|
589
|
-
: null;
|
|
590
|
-
const secondPartyUser = secondPartyActorForHeadline
|
|
591
|
-
? userMap.get(secondPartyActorForHeadline.userId) ?? null
|
|
592
|
-
: null;
|
|
593
|
-
const secondPartyNameForHeadline = secondPartyActorForHeadline
|
|
594
|
-
? (profileMap.get(secondPartyActorForHeadline.userId)?.identity?.name ??
|
|
595
|
-
secondPartyUser?.name ??
|
|
596
|
-
undefined)
|
|
597
|
-
: undefined;
|
|
598
|
-
const viewerActor = opp.actors.find((a) => a.userId === context.userId);
|
|
599
|
-
const viewerRole = viewerActor?.role ?? "party";
|
|
600
|
-
const isCounterpartGhost = counterpartUser?.isGhost ?? false;
|
|
601
|
-
const [ctx, negotiationContext] = await Promise.all([
|
|
602
|
-
gatherOpportunityPresenterContext(presenterDb, opp, context.userId, counterpartUserId),
|
|
603
|
-
loadNegotiationContext(deps.negotiationDatabase, opp.id, opp.status),
|
|
604
|
-
]);
|
|
605
|
-
const presentation = await presenter.presentCard({
|
|
606
|
-
...ctx,
|
|
607
|
-
opportunityStatus: opp.status,
|
|
608
|
-
...(negotiationContext ? { negotiationContext } : {}),
|
|
609
|
-
});
|
|
610
|
-
let narratorChip;
|
|
611
|
-
const introducerIsCounterpart = introducerActor && counterpartActor && introducerActor.userId === counterpartActor.userId;
|
|
612
|
-
if (introducerActor && introducerActor.userId !== context.userId && !introducerIsCounterpart) {
|
|
613
|
-
const narratorName = introducerName?.trim() || "Someone";
|
|
614
|
-
narratorChip = {
|
|
615
|
-
name: narratorName,
|
|
616
|
-
text: stripLeadingNarratorName(presentation.narratorRemark, narratorName),
|
|
617
|
-
avatar: introducerUser?.avatar ?? null,
|
|
618
|
-
userId: introducerActor.userId,
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
else if (introducerActor?.userId === context.userId) {
|
|
622
|
-
narratorChip = { name: "You", text: presentation.narratorRemark, userId: context.userId };
|
|
623
|
-
}
|
|
624
|
-
else {
|
|
625
|
-
narratorChip = { name: "Index", text: presentation.narratorRemark };
|
|
626
|
-
}
|
|
627
|
-
const cardData = {
|
|
628
|
-
opportunityId: opp.id,
|
|
629
|
-
userId: counterpartUserId,
|
|
630
|
-
name: counterpartName,
|
|
631
|
-
avatar: counterpartUser?.avatar ?? null,
|
|
632
|
-
mainText: stripUuids(presentation.personalizedSummary),
|
|
633
|
-
cta: presentation.suggestedAction,
|
|
634
|
-
headline: viewerIsIntroducerHere && secondPartyNameForHeadline
|
|
635
|
-
? `${counterpartName} → ${secondPartyNameForHeadline}`
|
|
636
|
-
: presentation.headline,
|
|
637
|
-
primaryActionLabel: getPrimaryActionLabel(viewerRole),
|
|
638
|
-
secondaryActionLabel: SECONDARY_ACTION_LABEL,
|
|
639
|
-
mutualIntentsLabel: presentation.mutualIntentsLabel,
|
|
640
|
-
narratorChip,
|
|
641
|
-
viewerRole,
|
|
642
|
-
score: typeof opp.interpretation?.confidence === "number"
|
|
643
|
-
? opp.interpretation.confidence
|
|
644
|
-
: undefined,
|
|
645
|
-
status: opp.status,
|
|
646
|
-
isGhost: isCounterpartGhost,
|
|
647
|
-
...(viewerIsIntroducerHere && secondPartyNameForHeadline
|
|
648
|
-
? {
|
|
649
|
-
secondParty: {
|
|
650
|
-
name: secondPartyNameForHeadline,
|
|
651
|
-
...(secondPartyUser?.avatar != null ? { avatar: secondPartyUser.avatar } : {}),
|
|
652
|
-
...(secondPartyActorForHeadline?.userId ? { userId: secondPartyActorForHeadline.userId } : {}),
|
|
653
|
-
},
|
|
654
|
-
}
|
|
655
|
-
: {}),
|
|
656
|
-
};
|
|
657
|
-
// For MCP callers, attach the agent-facing profile link and the
|
|
658
|
-
// opportunity deep link so the agent never has to fabricate
|
|
659
|
-
// either. Accepting still happens in the Index app behind an
|
|
660
|
-
// authenticated call — the deep link only opens the card.
|
|
661
|
-
if (context.isMcp) {
|
|
662
|
-
attachProfileLink(cardData, {
|
|
663
|
-
counterpartUserId,
|
|
664
|
-
frontendUrl: deps.frontendUrl,
|
|
665
|
-
});
|
|
666
|
-
attachOpportunityAppLink(cardData, {
|
|
667
|
-
frontendUrl: deps.frontendUrl,
|
|
668
|
-
});
|
|
669
|
-
}
|
|
670
|
-
return cardData;
|
|
671
|
-
}
|
|
672
|
-
catch (err) {
|
|
673
|
-
logger.warn("Skipping opportunity that failed to build presenter card", {
|
|
674
|
-
opportunityId: opp.id,
|
|
675
|
-
err,
|
|
676
|
-
});
|
|
677
|
-
skippedIds.push(opp.id);
|
|
678
|
-
return null;
|
|
679
|
-
}
|
|
680
|
-
}));
|
|
681
|
-
for (const card of chunkCards) {
|
|
682
|
-
if (card)
|
|
683
|
-
cardDataList.push(card);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
const listDebugSteps = buildListDebugSteps();
|
|
688
|
-
if (cardDataList.length === 0) {
|
|
689
|
-
if (skippedIds.length > 0) {
|
|
690
|
-
return success({
|
|
691
|
-
found: false,
|
|
692
|
-
count: 0,
|
|
693
|
-
summary: "Some opportunities couldn't be displayed",
|
|
694
|
-
message: "I found opportunities, but couldn't render them. Please try again.",
|
|
695
|
-
...(listDebugSteps.length ? { debugSteps: listDebugSteps } : {}),
|
|
696
|
-
});
|
|
697
|
-
}
|
|
698
|
-
return success({
|
|
699
|
-
found: false,
|
|
700
|
-
count: 0,
|
|
701
|
-
summary: "No opportunities yet",
|
|
702
|
-
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.",
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
return success({
|
|
706
|
-
found: true,
|
|
707
|
-
count: cardDataList.length,
|
|
708
|
-
summary: `You have ${cardDataList.length} opportunity(ies)`,
|
|
709
|
-
message: buildOpportunityPresentation(cardDataList, {
|
|
710
|
-
isMcp: context.isMcp ?? false,
|
|
711
|
-
leadIn: `You have ${cardDataList.length} opportunity(ies).`,
|
|
712
|
-
includeDigestMarkers: context.isMcp === true && query.includeDigestMarkers === true,
|
|
713
|
-
}),
|
|
714
|
-
...(listDebugSteps.length ? { debugSteps: listDebugSteps } : {}),
|
|
715
|
-
});
|
|
716
|
-
},
|
|
717
|
-
});
|
|
28
|
+
const listOpportunities = createListOpportunitiesTool(defineTool, deps);
|
|
718
29
|
const updateOpportunity = defineTool({
|
|
719
30
|
name: "update_opportunity",
|
|
720
31
|
description: "Updates an opportunity's status, advancing it through the connection lifecycle.\n\n" +
|
|
@@ -885,12 +196,17 @@ export function createOpportunityTools(defineTool, deps) {
|
|
|
885
196
|
const _updateGraphStart = Date.now();
|
|
886
197
|
const _updateTraceEmitter = requestContext.getStore()?.traceEmitter;
|
|
887
198
|
_updateTraceEmitter?.({ type: "graph_start", name: "opportunity" });
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
199
|
+
const operations = deps.opportunityOperations ?? { sendOpportunity, updateOpportunityStatus };
|
|
200
|
+
const result = isSend
|
|
201
|
+
? await operations.sendOpportunity(deps, {
|
|
202
|
+
userId: context.userId,
|
|
203
|
+
opportunityId: query.opportunityId,
|
|
204
|
+
})
|
|
205
|
+
: await operations.updateOpportunityStatus(deps, {
|
|
206
|
+
userId: context.userId,
|
|
207
|
+
opportunityId: query.opportunityId,
|
|
208
|
+
newStatus: query.status,
|
|
209
|
+
});
|
|
894
210
|
const _updateGraphMs = Date.now() - _updateGraphStart;
|
|
895
211
|
_updateTraceEmitter?.({ type: "graph_end", name: "opportunity", durationMs: _updateGraphMs });
|
|
896
212
|
if (result.mutationResult) {
|
|
@@ -903,7 +219,8 @@ export function createOpportunityTools(defineTool, deps) {
|
|
|
903
219
|
...(result.mutationResult.conversationId && {
|
|
904
220
|
conversationId: result.mutationResult.conversationId,
|
|
905
221
|
}),
|
|
906
|
-
|
|
222
|
+
// Neither update nor send invokes an agent, so this stage never reports sub-agent timings.
|
|
223
|
+
_graphTimings: [{ name: 'opportunity', durationMs: _updateGraphMs, agents: [] }],
|
|
907
224
|
});
|
|
908
225
|
}
|
|
909
226
|
return error(result.mutationResult.error || "Failed to update opportunity.");
|