@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,417 @@
1
+ /**
2
+ * Enrichment tools that create or update a user context.
3
+ */
4
+ import { z } from "zod";
5
+ import { requestContext } from "../shared/observability/request-context.js";
6
+ import { success, error, needsClarification } from "../shared/agent/tool.helpers.js";
7
+ import { detectSocialLabel } from "../shared/utils/social-label.js";
8
+ import { invokeWithAbortSignal } from "../shared/agent/model-signal.js";
9
+ import { enqueueEnrichmentRun, enrichFromUserRecord, isMeaningfulEnrichment, logger, markApprovedProfileConfirmed, mergeUserSocials, socialsRecordToRows } from "./enrichment.tools.helpers.js";
10
+ export function createUserContextWriteTools(defineTool, deps) {
11
+ const { userDb, systemDb, graphs, enricher, grantDefaultSystemPermissions, reportToolError, getUserContextText } = deps;
12
+ const createUserContext = defineTool({
13
+ name: "create_user_context",
14
+ description: "Legacy/backward-compatible tool that creates or regenerates the authenticated user's profile. Prefer " +
15
+ "preview_user_context → confirm_user_context so the draft is shown before saving. " +
16
+ "Profiles are essential for discovery — they provide the semantic context used to match users with complementary intents.\n\n" +
17
+ "**How it works:** For generic clients, the system can enrich profile data from public web sources (LinkedIn, GitHub, Twitter) and/or explicit user input, " +
18
+ "then generates a structured profile with bio, skills, interests, location, and narrative context.\n\n" +
19
+ "**Usage patterns:**\n" +
20
+ "- No args: attempts auto-generation from account data. If insufficient info, returns `missingFields` — ask the user for name/social URLs and retry.\n" +
21
+ "- With social URLs (linkedinUrl, githubUrl, etc.): enriches from those specific URLs.\n" +
22
+ "- With bioOrDescription: creates profile from explicit text only (no web scraping).\n" +
23
+ "- First call may return a preview. Prefer preview_user_context instead because it does not persist enrichment side effects.\n\n" +
24
+ "**Returns:** The generated profile (name, bio, location, skills, interests) or a `needsClarification` response listing missing fields.\n\n" +
25
+ "**Next steps:** After profile creation, the user can create intents (create_intent) and join indexes (create_network_membership) to start discovering opportunities.",
26
+ querySchema: z.object({
27
+ name: z.string().optional().describe("User's full name (first and last). Pass when the user explicitly provides their name."),
28
+ linkedinUrl: z.string().optional().describe("LinkedIn profile URL (e.g. 'https://linkedin.com/in/username'). Pass when user shares a LinkedIn link."),
29
+ githubUrl: z.string().optional().describe("GitHub profile URL (e.g. 'https://github.com/username'). Pass when user shares a GitHub link."),
30
+ twitterUrl: z.string().optional().describe("X/Twitter profile URL (e.g. 'https://x.com/username'). Pass when user shares a Twitter/X link."),
31
+ websites: z.array(z.string()).optional().describe("Personal or portfolio website URLs. Pass when user shares website links."),
32
+ location: z.string().optional().describe("User's location (e.g. 'Berlin, Germany' or 'SF Bay Area'). Pass when the user mentions where they are based."),
33
+ bioOrDescription: z.string().optional().describe("Explicit profile text from the user (e.g. 'software engineer focused on AI/ML, based in SF'). When provided, creates/updates profile from this text only — no web scraping. Use when user describes themselves in chat."),
34
+ confirm: z.boolean().optional().describe("Set to true to save a previously previewed profile after the user approves the preview."),
35
+ }),
36
+ handler: async ({ context, query }) => {
37
+ // Persist user-info fields (name, location, socials) to users table before any branching.
38
+ // This ensures users.name is always updated regardless of which code path runs.
39
+ // Trim all string fields to avoid persisting whitespace-only values.
40
+ const name = query.name?.trim();
41
+ const location = query.location?.trim();
42
+ const linkedinUrl = query.linkedinUrl?.trim();
43
+ const githubUrl = query.githubUrl?.trim();
44
+ const twitterUrl = query.twitterUrl?.trim();
45
+ const websites = query.websites?.map((url) => url.trim()).filter(Boolean);
46
+ const hasSocialsFromQuery = Boolean(linkedinUrl || githubUrl || twitterUrl || websites?.length);
47
+ if (name || location) {
48
+ await userDb.updateUser({
49
+ ...(name ? { name } : {}),
50
+ ...(location ? { location } : {}),
51
+ });
52
+ }
53
+ if (hasSocialsFromQuery) {
54
+ const newSocials = [];
55
+ if (linkedinUrl)
56
+ newSocials.push({ label: 'linkedin', value: linkedinUrl });
57
+ if (githubUrl)
58
+ newSocials.push({ label: 'github', value: githubUrl });
59
+ if (twitterUrl)
60
+ newSocials.push({ label: 'twitter', value: twitterUrl });
61
+ if (websites?.length) {
62
+ for (const w of websites)
63
+ newSocials.push({ label: detectSocialLabel(w), value: w });
64
+ }
65
+ await mergeUserSocials(deps, newSocials);
66
+ }
67
+ logger.verbose("Persisted user-info fields to user record", { userId: context.userId });
68
+ const isOnboarding = !(context.user.onboarding?.completedAt);
69
+ if (isOnboarding) {
70
+ // "Already enriched?" must key on a real enrichment signal, not getProfile():
71
+ // post-WS11 getProfile() returns a presentation row for EVERY existing user, so
72
+ // it would always short-circuit onboarding and refuse to enrich. The global
73
+ // user_context is the canonical signal (non-empty <=> the user has premises /
74
+ // has been enriched), mirroring findWithGraph's `hasProfile`.
75
+ const existingContext = getUserContextText
76
+ ? (await getUserContextText(context.userId)).trim()
77
+ : '';
78
+ if (existingContext) {
79
+ const existingProfile = await userDb.getProfile();
80
+ return success({
81
+ alreadyExists: true,
82
+ message: context.isMcp
83
+ ? "Profile already exists. If they want changes, use create_user_context(bioOrDescription=\"...\", confirm=true)."
84
+ : "Profile already exists. If the user confirmed it, call complete_onboarding() to finish setup. If they want changes, use create_user_context(bioOrDescription=\"...\", confirm=true).",
85
+ ...(existingProfile
86
+ ? {
87
+ profile: {
88
+ name: existingProfile.identity.name,
89
+ bio: existingProfile.identity.bio,
90
+ location: existingProfile.identity.location,
91
+ },
92
+ }
93
+ : {}),
94
+ });
95
+ }
96
+ // Preview mode: enrich and persist enrichment results, but don't generate full profile
97
+ if (!query.confirm) {
98
+ try {
99
+ const user = await userDb.getUser();
100
+ const enrichment = user ? await enrichFromUserRecord(deps, user) : null;
101
+ if (isMeaningfulEnrichment(enrichment)) {
102
+ // Persist enrichment data to user record so confirm path has it
103
+ const updatePayload = {};
104
+ if (enrichment.identity.name?.trim()) {
105
+ updatePayload.name = enrichment.identity.name.trim();
106
+ }
107
+ if (enrichment.identity.bio?.trim())
108
+ updatePayload.intro = enrichment.identity.bio.trim();
109
+ if (enrichment.identity.location?.trim())
110
+ updatePayload.location = enrichment.identity.location.trim();
111
+ if (Object.keys(updatePayload).length > 0)
112
+ await userDb.updateUser(updatePayload);
113
+ const enrichedSocials = [];
114
+ if (enrichment.socials.twitter)
115
+ enrichedSocials.push({ label: 'twitter', value: enrichment.socials.twitter });
116
+ if (enrichment.socials.linkedin)
117
+ enrichedSocials.push({ label: 'linkedin', value: enrichment.socials.linkedin });
118
+ if (enrichment.socials.github)
119
+ enrichedSocials.push({ label: 'github', value: enrichment.socials.github });
120
+ if (enrichment.socials.telegram)
121
+ enrichedSocials.push({ label: 'telegram', value: enrichment.socials.telegram });
122
+ if (enrichment.socials.websites?.length) {
123
+ for (const w of enrichment.socials.websites)
124
+ enrichedSocials.push({ label: 'custom', value: w });
125
+ }
126
+ if (enrichedSocials.length > 0) {
127
+ await mergeUserSocials(deps, enrichedSocials);
128
+ }
129
+ return success({
130
+ preview: true,
131
+ message: "Profile preview generated. Call create_user_context(confirm=true) to save.",
132
+ profile: {
133
+ name: enrichment.identity.name,
134
+ bio: enrichment.identity.bio,
135
+ location: enrichment.identity.location,
136
+ skills: enrichment.attributes.skills,
137
+ interests: enrichment.attributes.interests,
138
+ },
139
+ // Always present when isMeaningfulEnrichment passes — may be {} if the
140
+ // enrichment found no social handles. LLM should ask the user to provide
141
+ // links when empty (see buildOnboarding step 3 in chat.prompt.ts).
142
+ detectedSocials: enrichment.socials,
143
+ });
144
+ }
145
+ }
146
+ catch (err) {
147
+ logger.warn("Enrichment preview failed", {
148
+ error: err instanceof Error ? err.message : String(err),
149
+ });
150
+ }
151
+ return needsClarification({
152
+ missingFields: ['bio_or_social_urls'],
153
+ message: "I couldn't find enough public info. Could you share a short description of yourself, or a LinkedIn/GitHub/X profile link?",
154
+ });
155
+ }
156
+ // Confirm mode: invoke graph in generate mode (enrichment data already persisted during preview)
157
+ // Do NOT re-run enrichFromUserRecord — the graph's autoGenerateNode handles enrichment
158
+ // from the (now well-populated) user record, avoiding non-deterministic drift.
159
+ try {
160
+ const _confirmGraphStart = Date.now();
161
+ const _confirmTraceEmitter = requestContext.getStore()?.traceEmitter;
162
+ _confirmTraceEmitter?.({ type: "graph_start", name: "enrichment" });
163
+ const result = await invokeWithAbortSignal(graphs.profile, {
164
+ userId: context.userId,
165
+ operationMode: 'generate',
166
+ });
167
+ const _confirmGraphMs = Date.now() - _confirmGraphStart;
168
+ _confirmTraceEmitter?.({ type: "graph_end", name: "enrichment", durationMs: _confirmGraphMs });
169
+ if (result.error)
170
+ return error(result.error);
171
+ if (result.profile) {
172
+ await markApprovedProfileConfirmed(deps, context);
173
+ return success({
174
+ created: true,
175
+ message: "Profile saved.",
176
+ profile: {
177
+ name: result.profile.identity.name,
178
+ bio: result.profile.identity.bio,
179
+ location: result.profile.identity.location,
180
+ skills: result.profile.attributes.skills,
181
+ interests: result.profile.attributes.interests,
182
+ },
183
+ _graphTimings: [{ name: 'enrichment', durationMs: _confirmGraphMs, agents: result.agentTimings ?? [] }],
184
+ });
185
+ }
186
+ }
187
+ catch (err) {
188
+ logger.warn("Profile generation on confirm failed, falling back to full graph", {
189
+ error: err instanceof Error ? err.message : String(err),
190
+ });
191
+ }
192
+ // Fallback: graph invocation failed on confirm, fall through to full graph invocation
193
+ }
194
+ const hasBioOrDescription = !!query.bioOrDescription?.trim();
195
+ if (hasBioOrDescription) {
196
+ // Create/update profile from user's explicit text only; do not persist to user record
197
+ // Include name and location in the input if provided so the EnrichmentGenerator can use them
198
+ const inputParts = [];
199
+ if (name)
200
+ inputParts.push(`Name: ${name}`);
201
+ if (location)
202
+ inputParts.push(`Location: ${location}`);
203
+ inputParts.push(query.bioOrDescription.trim());
204
+ const profileInput = inputParts.join('\n');
205
+ const _bioProfileGraphStart = Date.now();
206
+ const _bioProfileTraceEmitter = requestContext.getStore()?.traceEmitter;
207
+ _bioProfileTraceEmitter?.({ type: "graph_start", name: "enrichment" });
208
+ const result = await invokeWithAbortSignal(graphs.profile, {
209
+ userId: context.userId,
210
+ operationMode: 'write',
211
+ input: profileInput,
212
+ forceUpdate: true,
213
+ });
214
+ const _bioProfileGraphMs = Date.now() - _bioProfileGraphStart;
215
+ _bioProfileTraceEmitter?.({ type: "graph_end", name: "enrichment", durationMs: _bioProfileGraphMs });
216
+ if (result.error) {
217
+ return error(result.error);
218
+ }
219
+ if (result.profile) {
220
+ if (isOnboarding && query.confirm)
221
+ await markApprovedProfileConfirmed(deps, context);
222
+ return success({
223
+ created: true,
224
+ message: "Profile created/updated with the information you provided.",
225
+ profile: {
226
+ name: result.profile.identity.name,
227
+ bio: result.profile.identity.bio,
228
+ location: result.profile.identity.location,
229
+ skills: result.profile.attributes.skills,
230
+ interests: result.profile.attributes.interests,
231
+ },
232
+ _graphTimings: [{ name: 'enrichment', durationMs: _bioProfileGraphMs, agents: result.agentTimings ?? [] }],
233
+ });
234
+ }
235
+ if (isOnboarding && query.confirm)
236
+ await markApprovedProfileConfirmed(deps, context);
237
+ return success({
238
+ created: true,
239
+ message: "Profile created/updated with the information you provided.",
240
+ _graphTimings: [{ name: 'enrichment', durationMs: _bioProfileGraphMs, agents: result.agentTimings ?? [] }],
241
+ });
242
+ }
243
+ // Invoke profile graph in generate mode (uses enrichUserProfile Chat API)
244
+ const _generateProfileGraphStart = Date.now();
245
+ const _generateProfileTraceEmitter = requestContext.getStore()?.traceEmitter;
246
+ _generateProfileTraceEmitter?.({ type: "graph_start", name: "enrichment" });
247
+ const result = await invokeWithAbortSignal(graphs.profile, {
248
+ userId: context.userId,
249
+ operationMode: 'generate',
250
+ forceUpdate: true,
251
+ });
252
+ const _generateProfileGraphMs = Date.now() - _generateProfileGraphStart;
253
+ _generateProfileTraceEmitter?.({ type: "graph_end", name: "enrichment", durationMs: _generateProfileGraphMs });
254
+ // If user info is insufficient, ask conversationally
255
+ if (result.needsUserInfo) {
256
+ return needsClarification({
257
+ missingFields: result.missingUserInfo || ['social_urls', 'full_name'],
258
+ message: "I need a bit more information to create your profile. Could you share your full name and any social links (LinkedIn, GitHub, or X/Twitter)?",
259
+ });
260
+ }
261
+ if (result.error) {
262
+ return error(result.error);
263
+ }
264
+ if (result.profile) {
265
+ if (isOnboarding && query.confirm)
266
+ await markApprovedProfileConfirmed(deps, context);
267
+ return success({
268
+ created: true,
269
+ message: "Profile generated from your account data.",
270
+ profile: {
271
+ name: result.profile.identity.name,
272
+ bio: result.profile.identity.bio,
273
+ location: result.profile.identity.location,
274
+ skills: result.profile.attributes.skills,
275
+ interests: result.profile.attributes.interests,
276
+ },
277
+ _graphTimings: [{ name: 'enrichment', durationMs: _generateProfileGraphMs, agents: result.agentTimings ?? [] }],
278
+ });
279
+ }
280
+ return error("Failed to create profile. Please try again.");
281
+ },
282
+ });
283
+ const updateUserContext = defineTool({
284
+ name: "update_user_context",
285
+ description: "Updates the authenticated user's existing profile using a verb-style instruction interface.\n\n" +
286
+ "**How to use it:**\n" +
287
+ "- `action`: a natural-language instruction describing what to change (e.g. \"add interests\", \"update bio\", \"remove skill\", \"set location\").\n" +
288
+ "- `details`: the content to apply (e.g. \"procedural generation, roguelikes, narrative games\").\n" +
289
+ "- `socials`: optional social handles to merge into the user's reachable profile (e.g. `{ telegram: \"@alice\" }`).\n\n" +
290
+ "**Examples:**\n" +
291
+ "- `action=\"add interests\"`, `details=\"procedural generation, roguelikes\"`\n" +
292
+ "- `action=\"update bio\"`, `details=\"Product designer focused on desktop CRPG interfaces\"`\n" +
293
+ "- `action=\"set location\"`, `details=\"Berlin\"`\n" +
294
+ "- `action=\"remove all mentions of X\"` — existing profile facts matching X are retracted; the profile text regenerates without them shortly after.\n" +
295
+ "- `socials={ telegram: \"@alice\" }` to silently add a reachable chat handle without regenerating the profile.\n\n" +
296
+ "**When to use:** When the user wants to make specific changes without regenerating the whole profile. For full profile regeneration from social URLs, use create_user_context instead.\n\n" +
297
+ "**Important:** If the user provides a URL to update from, call scrape_url first, then pass the scraped content in `details`.\n\n" +
298
+ "**MCP behavior:** For MCP clients, text/profile graph updates are accepted immediately and completed in the background to avoid transport timeouts. Social-only updates still complete synchronously.\n\n" +
299
+ "**Returns:** Confirmation that the profile was updated or accepted for background update.",
300
+ querySchema: z.object({
301
+ profileId: z.string().optional().describe("Profile UUID from read_user_contexts. Omit to update the current user's own profile (most common usage)."),
302
+ action: z.string().optional().describe("Natural language description of ALL changes to make in a single call. Examples: 'update bio to focus on AI research', 'add Python and Rust to skills', 'change location to Berlin and add machine learning to interests'. Optional when only updating socials."),
303
+ details: z.string().optional().describe("Additional context or content to incorporate. Use this to pass scraped URL content (from scrape_url) or longer text the user provided."),
304
+ socials: z.record(z.string()).optional().describe("Social handles or URLs to merge into the user profile, keyed by label. Example: { telegram: '@alice', github: 'alice' }. Existing socials with other labels are preserved."),
305
+ }),
306
+ handler: async ({ context, query }) => {
307
+ const socialUpdates = socialsRecordToRows(query.socials);
308
+ const inputForProfile = [query.action, query.details].filter(Boolean).join("\n");
309
+ if (!inputForProfile.trim()) {
310
+ if (socialUpdates.length > 0) {
311
+ await mergeUserSocials(deps, socialUpdates);
312
+ return success({ message: "Profile socials updated." });
313
+ }
314
+ return error("Please specify what to update (e.g. action: 'update bio to X') or provide socials.");
315
+ }
316
+ const profileRunId = await enqueueEnrichmentRun(deps, context, "update_user_context", query);
317
+ if (profileRunId) {
318
+ return success({
319
+ status: "queued",
320
+ profileRunId,
321
+ message: `Profile update started. Call get_enrichment_run with profileRunId="${profileRunId}" until it succeeds, fails, or is cancelled.`,
322
+ });
323
+ }
324
+ // Use profileGraph query mode to validate profile existence and get id
325
+ const _updateQueryProfileGraphStart = Date.now();
326
+ const _updateQueryProfileTraceEmitter = requestContext.getStore()?.traceEmitter;
327
+ _updateQueryProfileTraceEmitter?.({ type: "graph_start", name: "enrichment" });
328
+ const queryResult = await invokeWithAbortSignal(graphs.profile, { userId: context.userId, operationMode: 'query' });
329
+ const _updateQueryProfileGraphMs = Date.now() - _updateQueryProfileGraphStart;
330
+ _updateQueryProfileTraceEmitter?.({ type: "graph_end", name: "enrichment", durationMs: _updateQueryProfileGraphMs });
331
+ if (!queryResult.readResult?.hasProfile && !queryResult.profile) {
332
+ return error("You don't have a profile yet. Use create_user_context first.");
333
+ }
334
+ const existingProfileId = queryResult.readResult?.profile?.id;
335
+ const providedProfileId = query.profileId?.trim();
336
+ if (providedProfileId && existingProfileId && providedProfileId !== existingProfileId) {
337
+ return error("Invalid profileId. Use the profile id from read_user_contexts.");
338
+ }
339
+ if (socialUpdates.length > 0) {
340
+ await mergeUserSocials(deps, socialUpdates);
341
+ }
342
+ if (context.isMcp) {
343
+ const _backgroundWriteProfileGraphStart = Date.now();
344
+ const _backgroundWriteProfileTraceEmitter = requestContext.getStore()?.traceEmitter;
345
+ _backgroundWriteProfileTraceEmitter?.({ type: "graph_start", name: "enrichment" });
346
+ graphs.profile.invoke({
347
+ userId: context.userId,
348
+ operationMode: "write",
349
+ input: inputForProfile,
350
+ forceUpdate: true,
351
+ }).then((writeResult) => {
352
+ if (writeResult.error) {
353
+ logger.error("Background profile update failed", {
354
+ userId: context.userId,
355
+ error: writeResult.error,
356
+ });
357
+ reportToolError?.(new Error(writeResult.error), {
358
+ subsystem: "enrichment",
359
+ operation: "profile.update_background",
360
+ toolName: "update_user_context",
361
+ userId: context.userId,
362
+ tags: { toolName: "update_user_context", execution: "background" },
363
+ context: { profileId: existingProfileId ?? providedProfileId },
364
+ });
365
+ }
366
+ }).catch((err) => {
367
+ const message = err instanceof Error ? err.message : String(err);
368
+ logger.error("Background profile update failed", {
369
+ userId: context.userId,
370
+ error: message,
371
+ });
372
+ reportToolError?.(err, {
373
+ subsystem: "enrichment",
374
+ operation: "profile.update_background",
375
+ toolName: "update_user_context",
376
+ userId: context.userId,
377
+ tags: { toolName: "update_user_context", execution: "background" },
378
+ context: { profileId: existingProfileId ?? providedProfileId },
379
+ });
380
+ }).finally(() => {
381
+ const _backgroundWriteProfileGraphMs = Date.now() - _backgroundWriteProfileGraphStart;
382
+ _backgroundWriteProfileTraceEmitter?.({ type: "graph_end", name: "enrichment", durationMs: _backgroundWriteProfileGraphMs });
383
+ });
384
+ return success({
385
+ accepted: true,
386
+ message: "Profile update accepted. The structured profile will refresh in the background.",
387
+ _graphTimings: [
388
+ { name: 'enrichment', durationMs: _updateQueryProfileGraphMs, agents: queryResult.agentTimings ?? [] },
389
+ ],
390
+ });
391
+ }
392
+ // Execute update directly
393
+ const _updateWriteProfileGraphStart = Date.now();
394
+ const _updateWriteProfileTraceEmitter = requestContext.getStore()?.traceEmitter;
395
+ _updateWriteProfileTraceEmitter?.({ type: "graph_start", name: "enrichment" });
396
+ const _writeResult = await invokeWithAbortSignal(graphs.profile, {
397
+ userId: context.userId,
398
+ operationMode: "write",
399
+ input: inputForProfile,
400
+ forceUpdate: true,
401
+ });
402
+ const _updateWriteProfileGraphMs = Date.now() - _updateWriteProfileGraphStart;
403
+ _updateWriteProfileTraceEmitter?.({ type: "graph_end", name: "enrichment", durationMs: _updateWriteProfileGraphMs });
404
+ if (_writeResult.error) {
405
+ return error(_writeResult.error);
406
+ }
407
+ return success({
408
+ message: "Profile updated.",
409
+ _graphTimings: [
410
+ { name: 'enrichment', durationMs: _updateQueryProfileGraphMs, agents: queryResult.agentTimings ?? [] },
411
+ { name: 'enrichment', durationMs: _updateWriteProfileGraphMs, agents: _writeResult.agentTimings ?? [] },
412
+ ],
413
+ });
414
+ },
415
+ });
416
+ return { createUserContext, updateUserContext };
417
+ }
@@ -1,3 +1,13 @@
1
+ /**
2
+ * The enrichment tool registry.
3
+ *
4
+ * The user-context read and write tools live in sibling modules and the shared
5
+ * helpers in `enrichment.tools.helpers.ts`; this file owns the enrichment-run
6
+ * and onboarding tools and assembles the registry.
7
+ */
1
8
  import type { DefineTool } from "../shared/agent/tool.helpers.js";
2
9
  import type { EnrichmentToolDeps } from "../contexts/ports/participant-context.tools.port.js";
3
- export declare function createEnrichmentTools(defineTool: DefineTool, deps: EnrichmentToolDeps): readonly [any, any, any, any, any, any, any, any];
10
+ import type { ToolSurface } from "../shared/agent/utility.tools.js";
11
+ export declare function createEnrichmentTools(defineTool: DefineTool, deps: EnrichmentToolDeps, options?: {
12
+ surface?: ToolSurface;
13
+ }): readonly [any, any, any, any, any, any, any, any];
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Helpers shared by the enrichment tools.
3
+ *
4
+ * These were nested functions inside `createEnrichmentTools`, closing over the
5
+ * destructured host capabilities. They are top-level now, each taking the tool
6
+ * deps explicitly, so the three tool modules can share them.
7
+ */
8
+ import { z } from "zod";
9
+ import type { ResolvedToolContext } from "../shared/agent/tool.helpers.js";
10
+ import type { EnrichmentToolDeps } from "../contexts/ports/participant-context.tools.port.js";
11
+ import type { EnrichmentResult } from "../shared/interfaces/enrichment.interface.js";
12
+ import type { OnboardingProfileSeed, OnboardingState, UserRecord } from "../shared/interfaces/database.interface.js";
13
+ import type { EnrichmentRunInput, EnrichmentRunOperation } from "../shared/interfaces/enrichment-run.interface.js";
14
+ export declare const logger: import("../shared/observability/log.js").LoggerWithSource;
15
+ export declare function isMeaningfulEnrichment(enrichment: EnrichmentResult | null): enrichment is EnrichmentResult;
16
+ export declare const approvedProfileDraftSchema: z.ZodObject<{
17
+ identity: z.ZodObject<{
18
+ name: z.ZodString;
19
+ bio: z.ZodString;
20
+ location: z.ZodString;
21
+ }, "strip", z.ZodTypeAny, {
22
+ name: string;
23
+ bio: string;
24
+ location: string;
25
+ }, {
26
+ name: string;
27
+ bio: string;
28
+ location: string;
29
+ }>;
30
+ narrative: z.ZodObject<{
31
+ context: z.ZodString;
32
+ }, "strip", z.ZodTypeAny, {
33
+ context: string;
34
+ }, {
35
+ context: string;
36
+ }>;
37
+ attributes: z.ZodObject<{
38
+ interests: z.ZodArray<z.ZodString, "many">;
39
+ skills: z.ZodArray<z.ZodString, "many">;
40
+ }, "strip", z.ZodTypeAny, {
41
+ skills: string[];
42
+ interests: string[];
43
+ }, {
44
+ skills: string[];
45
+ interests: string[];
46
+ }>;
47
+ }, "strip", z.ZodTypeAny, {
48
+ identity: {
49
+ name: string;
50
+ bio: string;
51
+ location: string;
52
+ };
53
+ narrative: {
54
+ context: string;
55
+ };
56
+ attributes: {
57
+ skills: string[];
58
+ interests: string[];
59
+ };
60
+ }, {
61
+ identity: {
62
+ name: string;
63
+ bio: string;
64
+ location: string;
65
+ };
66
+ narrative: {
67
+ context: string;
68
+ };
69
+ attributes: {
70
+ skills: string[];
71
+ interests: string[];
72
+ };
73
+ }>;
74
+ type ApprovedProfileDraft = z.infer<typeof approvedProfileDraftSchema>;
75
+ export declare function trimToUndefined(value: string | null | undefined): string | undefined;
76
+ export declare function isPlaceholderName(value: string): boolean;
77
+ export declare function enrichFromUserRecord(deps: EnrichmentToolDeps, user: {
78
+ name?: string | null;
79
+ email?: string | null;
80
+ socials: Array<{
81
+ id: string;
82
+ userId: string;
83
+ label: string;
84
+ value: string;
85
+ }>;
86
+ }): Promise<EnrichmentResult | null>;
87
+ export declare function selectProfileSeed(onboarding: OnboardingState | null | undefined, networkId?: string): OnboardingProfileSeed | undefined;
88
+ export declare function normalizeSocialUpdate(label: string, value: string): {
89
+ label: string;
90
+ value: string;
91
+ } | null;
92
+ export declare function mergeUserSocials(deps: EnrichmentToolDeps, incoming: {
93
+ label: string;
94
+ value: string;
95
+ }[]): Promise<void>;
96
+ export declare function socialsRecordToRows(socials: Record<string, string> | undefined): {
97
+ label: string;
98
+ value: string;
99
+ }[];
100
+ export declare function enqueueEnrichmentRun(deps: EnrichmentToolDeps, context: ResolvedToolContext, operation: EnrichmentRunOperation, input: EnrichmentRunInput): Promise<string | null>;
101
+ export declare function persistApprovedProfileContext(deps: EnrichmentToolDeps, profile: {
102
+ identity: {
103
+ name: string;
104
+ bio: string;
105
+ location: string;
106
+ };
107
+ }, user: UserRecord | null, networkId?: string): Promise<void>;
108
+ export declare function markApprovedProfileConfirmed(deps: EnrichmentToolDeps, context: ResolvedToolContext): Promise<void>;
109
+ export declare function buildProfileInput(parts: {
110
+ name?: string;
111
+ location?: string;
112
+ bioOrDescription?: string;
113
+ socials?: Array<{
114
+ label: string;
115
+ value: string;
116
+ }>;
117
+ }): string;
118
+ export declare function toProfileSummary(profile: {
119
+ identity: {
120
+ name: string;
121
+ bio: string;
122
+ location: string;
123
+ };
124
+ attributes: {
125
+ skills: string[];
126
+ interests: string[];
127
+ };
128
+ }): {
129
+ name: string;
130
+ bio: string;
131
+ location: string;
132
+ skills: string[];
133
+ interests: string[];
134
+ };
135
+ export declare function buildApprovedDraftProfileInput(draft: ApprovedProfileDraft): string;
136
+ export declare function decomposeApprovedDraftProfile(deps: EnrichmentToolDeps, profile: ApprovedProfileDraft & {
137
+ userId: string;
138
+ }): Promise<void>;
139
+ export {};