@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.
Files changed (184) hide show
  1. package/CHANGELOG.md +141 -0
  2. package/IMPLEMENTATION.md +50 -52
  3. package/STABILITY.md +3 -3
  4. package/dist/chat/chat.agent.js +2 -3
  5. package/dist/chat/chat.graph.d.ts +41 -1
  6. package/dist/chat/chat.graph.js +108 -127
  7. package/dist/chat/negotiator.persona.d.ts +1 -1
  8. package/dist/chat/negotiator.persona.js +1 -4
  9. package/dist/chat/negotiator.prompt.js +1 -2
  10. package/dist/chat/onboarding.persona.js +0 -1
  11. package/dist/chat/reporter.persona.js +0 -1
  12. package/dist/chat/signal.persona.js +0 -1
  13. package/dist/contacts/application/contact.tools.d.ts +6 -9
  14. package/dist/contacts/application/contact.tools.js +11 -92
  15. package/dist/contacts/application/index.d.ts +2 -14
  16. package/dist/contacts/application/index.js +2 -13
  17. package/dist/contacts/domain/contact.types.d.ts +0 -23
  18. package/dist/contacts/domain/index.d.ts +2 -6
  19. package/dist/contacts/index.d.ts +1 -1
  20. package/dist/contacts/index.js +1 -1
  21. package/dist/contacts/ports/contact.repository.port.d.ts +5 -12
  22. package/dist/contacts/ports/contact.repository.port.js +4 -6
  23. package/dist/contacts/ports/contact.tools.port.d.ts +1 -3
  24. package/dist/contacts/ports/contact.tools.port.js +1 -2
  25. package/dist/contacts/ports/index.d.ts +1 -1
  26. package/dist/contacts/ports/index.js +1 -1
  27. package/dist/discovery/hyde.frame.d.ts +4 -4
  28. package/dist/discovery/hyde.graph.d.ts +71 -12
  29. package/dist/discovery/hyde.graph.js +337 -335
  30. package/dist/discovery/lens.inferrer.d.ts +6 -6
  31. package/dist/enrichment/enrichment.graph.d.ts +127 -9
  32. package/dist/enrichment/enrichment.graph.js +633 -655
  33. package/dist/enrichment/enrichment.state.d.ts +1 -1
  34. package/dist/enrichment/enrichment.tools.context-read.d.ts +10 -0
  35. package/dist/enrichment/enrichment.tools.context-read.js +353 -0
  36. package/dist/enrichment/enrichment.tools.context-write.d.ts +9 -0
  37. package/dist/enrichment/enrichment.tools.context-write.js +417 -0
  38. package/dist/enrichment/enrichment.tools.d.ts +11 -1
  39. package/dist/enrichment/enrichment.tools.helpers.d.ts +139 -0
  40. package/dist/enrichment/enrichment.tools.helpers.js +234 -0
  41. package/dist/enrichment/enrichment.tools.js +16 -975
  42. package/dist/index.d.ts +27 -50
  43. package/dist/index.js +17 -28
  44. package/dist/intents/application/intent.graph.d.ts +71 -67
  45. package/dist/intents/application/intent.graph.execute.d.ts +59 -0
  46. package/dist/intents/application/intent.graph.execute.js +301 -0
  47. package/dist/intents/application/intent.graph.infer.d.ts +44 -0
  48. package/dist/intents/application/intent.graph.infer.js +97 -0
  49. package/dist/intents/application/intent.graph.js +96 -888
  50. package/dist/intents/application/intent.graph.reconcile.d.ts +71 -0
  51. package/dist/intents/application/intent.graph.reconcile.js +274 -0
  52. package/dist/intents/application/intent.graph.shared.d.ts +70 -0
  53. package/dist/intents/application/intent.graph.shared.js +153 -0
  54. package/dist/intents/domain/intent.state.d.ts +1 -1
  55. package/dist/maintenance/maintenance.graph.d.ts +66 -3
  56. package/dist/maintenance/maintenance.graph.js +155 -156
  57. package/dist/mcp/mcp.authorization-policy.d.ts +24 -28
  58. package/dist/mcp/mcp.authorization-policy.js +14 -34
  59. package/dist/mcp/mcp.server.d.ts +3 -3
  60. package/dist/mcp/mcp.server.js +14 -20
  61. package/dist/negotiations/application/negotiation.candidates.d.ts +83 -0
  62. package/dist/negotiations/application/negotiation.candidates.js +162 -0
  63. package/dist/negotiations/application/negotiation.graph.d.ts +91 -157
  64. package/dist/negotiations/application/negotiation.graph.finalize.d.ts +5 -0
  65. package/dist/negotiations/application/negotiation.graph.finalize.js +280 -0
  66. package/dist/negotiations/application/negotiation.graph.init.d.ts +54 -0
  67. package/dist/negotiations/application/negotiation.graph.init.js +227 -0
  68. package/dist/negotiations/application/negotiation.graph.js +69 -1388
  69. package/dist/negotiations/application/negotiation.graph.screen.d.ts +23 -0
  70. package/dist/negotiations/application/negotiation.graph.screen.js +108 -0
  71. package/dist/negotiations/application/negotiation.graph.shared.d.ts +75 -0
  72. package/dist/negotiations/application/negotiation.graph.shared.js +125 -0
  73. package/dist/negotiations/application/negotiation.graph.turn.d.ts +105 -0
  74. package/dist/negotiations/application/negotiation.graph.turn.js +484 -0
  75. package/dist/negotiations/domain/negotiation.state.d.ts +1 -1
  76. package/dist/negotiations/domain/negotiation.state.js +0 -1
  77. package/dist/networks/application/indexer.graph.d.ts +165 -7
  78. package/dist/networks/application/indexer.graph.js +339 -388
  79. package/dist/networks/application/indexer.state.d.ts +1 -1
  80. package/dist/networks/application/membership.graph.d.ts +83 -5
  81. package/dist/networks/application/membership.graph.js +177 -207
  82. package/dist/networks/application/network.graph.d.ts +148 -5
  83. package/dist/networks/application/network.graph.js +249 -278
  84. package/dist/networks/domain/membership.state.d.ts +1 -1
  85. package/dist/networks/domain/network.state.d.ts +1 -1
  86. package/dist/opportunities/application/delivery-card.cache.d.ts +1 -1
  87. package/dist/opportunities/application/delivery-card.cache.js +2 -2
  88. package/dist/opportunities/application/index.d.ts +2 -2
  89. package/dist/opportunities/application/index.js +2 -2
  90. package/dist/opportunities/application/opportunity.evaluator.js +1 -1
  91. package/dist/opportunities/application/opportunity.graph.d.ts +686 -473
  92. package/dist/opportunities/application/opportunity.graph.discovery-strategies.d.ts +109 -0
  93. package/dist/opportunities/application/opportunity.graph.discovery-strategies.js +451 -0
  94. package/dist/opportunities/application/opportunity.graph.discovery.d.ts +69 -0
  95. package/dist/opportunities/application/opportunity.graph.discovery.js +397 -0
  96. package/dist/opportunities/application/opportunity.graph.evaluation.d.ts +82 -0
  97. package/dist/opportunities/application/opportunity.graph.evaluation.js +608 -0
  98. package/dist/opportunities/application/opportunity.graph.js +120 -3690
  99. package/dist/opportunities/application/opportunity.graph.modes.d.ts +538 -0
  100. package/dist/opportunities/application/opportunity.graph.modes.js +538 -0
  101. package/dist/opportunities/application/opportunity.graph.negotiate.d.ts +109 -0
  102. package/dist/opportunities/application/opportunity.graph.negotiate.js +392 -0
  103. package/dist/opportunities/application/opportunity.graph.persist-node.d.ts +93 -0
  104. package/dist/opportunities/application/opportunity.graph.persist-node.js +765 -0
  105. package/dist/opportunities/application/opportunity.graph.prep.d.ts +150 -0
  106. package/dist/opportunities/application/opportunity.graph.prep.js +381 -0
  107. package/dist/opportunities/application/opportunity.graph.shared.d.ts +173 -0
  108. package/dist/opportunities/application/opportunity.graph.shared.js +199 -0
  109. package/dist/opportunities/application/opportunity.presentation.d.ts +399 -0
  110. package/dist/opportunities/application/{opportunity.presenter.js → opportunity.presentation.js} +735 -9
  111. package/dist/opportunities/application/opportunity.tools.cards.d.ts +152 -0
  112. package/dist/opportunities/application/opportunity.tools.cards.js +235 -0
  113. package/dist/opportunities/application/opportunity.tools.d.ts +8 -114
  114. package/dist/opportunities/application/opportunity.tools.js +28 -711
  115. package/dist/opportunities/application/opportunity.tools.list.d.ts +10 -0
  116. package/dist/opportunities/application/opportunity.tools.list.js +493 -0
  117. package/dist/opportunities/domain/index.d.ts +3 -3
  118. package/dist/opportunities/domain/index.js +3 -3
  119. package/dist/opportunities/domain/opportunity.state.d.ts +19 -19
  120. package/dist/opportunities/index.d.ts +6 -6
  121. package/dist/opportunities/index.js +4 -4
  122. package/dist/opportunities/ports/opportunity.tools.port.d.ts +1 -1
  123. package/dist/opportunities/radar/radar.graph.d.ts +57 -13
  124. package/dist/opportunities/radar/radar.graph.js +470 -471
  125. package/dist/premises/premise.graph.d.ts +88 -20
  126. package/dist/premises/premise.graph.js +207 -218
  127. package/dist/questions/domain/question.schema.d.ts +40 -40
  128. package/dist/shared/agent/tool.factory.js +0 -8
  129. package/dist/shared/agent/tool.helpers.d.ts +21 -45
  130. package/dist/shared/agent/tool.helpers.js +1 -2
  131. package/dist/shared/agent/tool.registry.d.ts +4 -3
  132. package/dist/shared/agent/tool.registry.js +2 -4
  133. package/dist/shared/agent/tool.runtime.js +0 -2
  134. package/dist/shared/agent/utility.tools.js +9 -13
  135. package/dist/shared/interfaces/database.capabilities.d.ts +100 -0
  136. package/dist/shared/interfaces/database.capabilities.js +7 -0
  137. package/dist/shared/interfaces/database.entities.d.ts +533 -0
  138. package/dist/shared/interfaces/database.entities.js +10 -0
  139. package/dist/shared/interfaces/database.identity-queries.d.ts +294 -0
  140. package/dist/shared/interfaces/database.identity-queries.js +4 -0
  141. package/dist/shared/interfaces/database.interface.d.ts +15 -2277
  142. package/dist/shared/interfaces/database.interface.js +8 -0
  143. package/dist/shared/interfaces/database.member-queries.d.ts +221 -0
  144. package/dist/shared/interfaces/database.member-queries.js +4 -0
  145. package/dist/shared/interfaces/database.negotiation.d.ts +305 -0
  146. package/dist/shared/interfaces/database.negotiation.js +26 -0
  147. package/dist/shared/interfaces/database.network-queries.d.ts +277 -0
  148. package/dist/shared/interfaces/database.network-queries.js +4 -0
  149. package/dist/shared/interfaces/database.opportunity-queries.d.ts +285 -0
  150. package/dist/shared/interfaces/database.opportunity-queries.js +4 -0
  151. package/dist/shared/interfaces/database.port.d.ts +322 -0
  152. package/dist/shared/interfaces/database.port.js +29 -0
  153. package/dist/shared/schemas/discovery-question.schema.d.ts +14 -14
  154. package/dist/shared/schemas/negotiation-digest.schema.d.ts +2 -2
  155. package/package.json +2 -15
  156. package/dist/contacts/application/contact.inviter.d.ts +0 -49
  157. package/dist/contacts/application/contact.inviter.js +0 -64
  158. package/dist/integrations/application/index.d.ts +0 -14
  159. package/dist/integrations/application/index.js +0 -14
  160. package/dist/integrations/application/integration.tools.d.ts +0 -27
  161. package/dist/integrations/application/integration.tools.js +0 -98
  162. package/dist/integrations/domain/index.d.ts +0 -16
  163. package/dist/integrations/domain/index.js +0 -1
  164. package/dist/integrations/domain/integration.types.d.ts +0 -51
  165. package/dist/integrations/domain/integration.types.js +0 -9
  166. package/dist/integrations/index.d.ts +0 -10
  167. package/dist/integrations/index.js +0 -8
  168. package/dist/integrations/ports/index.d.ts +0 -25
  169. package/dist/integrations/ports/index.js +0 -23
  170. package/dist/integrations/ports/integration.adapter.port.d.ts +0 -62
  171. package/dist/integrations/ports/integration.adapter.port.js +0 -15
  172. package/dist/integrations/ports/integration.importer.port.d.ts +0 -31
  173. package/dist/integrations/ports/integration.importer.port.js +0 -15
  174. package/dist/integrations/ports/integration.tools.port.d.ts +0 -25
  175. package/dist/integrations/ports/integration.tools.port.js +0 -18
  176. package/dist/opportunities/application/opportunity.card-presentation.d.ts +0 -44
  177. package/dist/opportunities/application/opportunity.card-presentation.js +0 -93
  178. package/dist/opportunities/application/opportunity.presenter.d.ts +0 -156
  179. package/dist/opportunities/domain/opportunity.presentation-cache.d.ts +0 -5
  180. package/dist/opportunities/domain/opportunity.presentation-cache.js +0 -11
  181. package/dist/opportunities/domain/opportunity.presentation.d.ts +0 -76
  182. package/dist/opportunities/domain/opportunity.presentation.js +0 -516
  183. package/dist/opportunities/domain/opportunity.safe-presentation.d.ts +0 -104
  184. package/dist/opportunities/domain/opportunity.safe-presentation.js +0 -102
@@ -1,890 +1,41 @@
1
+ /**
2
+ * Intent graph: prep → (query | inference → verification → reconciler → executor).
3
+ *
4
+ * Every node is a top-level function in a sibling module, taking the graph
5
+ * state and an explicit {@link IntentGraphDeps}. This file composes the
6
+ * dependency bag and wires the edges — nothing else.
7
+ */
1
8
  import { StateGraph, START, END } from "@langchain/langgraph";
2
- import { IntentGraphState } from "../domain/intent.state.js";
3
9
  import { ExplicitIntentInferrer } from "./intent.inferrer.js";
4
10
  import { SemanticVerifier } from "./intent.verifier.js";
5
- import { DEFAULT_SPECIFICITY_WARNING } from "../domain/signal.specificity.js";
6
- import { normalizeIntentDescription } from "../domain/intent.proposal.js";
7
11
  import { IntentReconciler } from "./intent.reconciler.js";
8
- import { getAbortSignalConfig } from "../../shared/agent/model-signal.js";
9
- import { protocolLogger } from "../../shared/observability/protocol.logger.js";
10
- import { timed } from "../../shared/observability/performance.js";
11
- import { requestContext } from "../../shared/observability/request-context.js";
12
- const logger = protocolLogger("IntentGraphFactory");
13
- /**
14
- * Enforce write-mode constraints on reconciler output before any action can
15
- * reach persistence. Update mode is deliberately fail-closed: only updates
16
- * whose id is one of the caller-provided targets survive.
17
- */
18
- export function enforceIntentActionBoundary(operationMode, targetIntentIds, actions) {
19
- if (operationMode !== 'update')
20
- return actions;
21
- const targets = new Set(targetIntentIds ?? []);
22
- return actions.filter((action) => action.type === 'update' && targets.has(action.id));
23
- }
24
- /**
25
- * Build the only action permitted for an explicit update. This path is
26
- * intentionally deterministic: semantic reconciliation may shape create
27
- * operations, but it may not redirect an update away from its supplied target.
28
- */
29
- export function buildExplicitUpdateActions(targetIntentIds, activeIntentIds, candidates) {
30
- if (targetIntentIds?.length !== 1 || !activeIntentIds.includes(targetIntentIds[0])) {
31
- return {
32
- actions: [],
33
- failure: {
34
- category: 'update_target_boundary',
35
- message: 'Explicit update requires exactly one active intent owned by the caller.',
36
- },
37
- };
38
- }
39
- if (candidates.length !== 1) {
40
- return {
41
- actions: [],
42
- failure: {
43
- category: 'reconciliation_boundary',
44
- message: 'Explicit update must resolve to exactly one verified intent.',
45
- },
46
- };
47
- }
48
- const candidate = candidates[0];
49
- return {
50
- actions: [{
51
- type: 'update',
52
- id: targetIntentIds[0],
53
- payload: candidate.description,
54
- score: candidate.score ?? null,
55
- reasoning: candidate.reasoning ?? 'Explicit user-confirmed update',
56
- intentMode: candidate.verification?.referential_anchor ? 'REFERENTIAL' : 'ATTRIBUTIVE',
57
- }],
58
- };
59
- }
60
- const MAX_PERMISSIBLE_ENTROPY = 0.75;
61
- const MIN_CLEAR_INTENT_SCORE = 40;
62
- const GENERIC_JOB_PHRASE = /\b(?:a|any|some)\s+job\b/i;
63
- const inferRoleFromContextText = (text) => {
64
- const normalized = text.toLowerCase();
65
- if (/\b(engineer|developer)\b/.test(normalized))
66
- return "software engineering";
67
- if (/\b(designer|ux|ui)\b/.test(normalized))
68
- return "product design";
69
- if (/\b(marketing|marketer|growth)\b/.test(normalized))
70
- return "marketing";
71
- if (/\b(product manager|product)\b/.test(normalized))
72
- return "product management";
73
- if (/\b(data scientist|machine learning|ml|ai)\b/.test(normalized))
74
- return "AI/ML";
75
- if (/\b(sales|account executive|business development)\b/.test(normalized))
76
- return "sales";
77
- return null;
78
- };
79
- /**
80
- * Derive a job-role qualifier from the user's global user_context paragraph
81
- * (the identity text that replaced the legacy profile projection). Role is
82
- * inferred from the free text; the old structured skills/interests extraction
83
- * is gone with the removed profile fields.
84
- */
85
- const buildJobQualifierFromContext = (contextText) => {
86
- const roleHint = inferRoleFromContextText(contextText ?? "");
87
- return roleHint ? `${roleHint} role` : null;
88
- };
89
- const enrichVagueIntentWithContext = (description, userContext) => {
90
- const trimmed = description?.trim();
91
- if (!trimmed)
92
- return description;
93
- const isGenericJobRequest = GENERIC_JOB_PHRASE.test(trimmed) ||
94
- /\b(?:find|get|look(?:ing)?\s+for|want)\s+(?:to\s+)?(?:find\s+)?job\b/i.test(trimmed);
95
- if (!isGenericJobRequest)
96
- return description;
97
- const qualifier = buildJobQualifierFromContext(userContext);
98
- if (!qualifier)
99
- return description;
100
- const enriched = trimmed
101
- .replace(/\ba job\b/i, `a ${qualifier}`)
102
- .replace(/\bjob\b/i, `${qualifier}`)
103
- .replace(/\s{2,}/g, " ")
104
- .trim();
105
- return enriched.length > 0 ? enriched : description;
106
- };
107
- const isVague = (description, entropy, clarity) => {
108
- if (GENERIC_JOB_PHRASE.test(description))
109
- return true;
110
- if (entropy > MAX_PERMISSIBLE_ENTROPY)
111
- return true;
112
- if (clarity < MIN_CLEAR_INTENT_SCORE)
113
- return true;
114
- return false;
115
- };
116
- const getSpecificityWarning = (verdict) => {
117
- const warning = verdict.specificity_warning?.trim();
118
- return warning && warning.length > 0 ? warning : DEFAULT_SPECIFICITY_WARNING;
119
- };
120
- const toSpeechActType = (classification) => {
121
- if (classification === "COMMISSIVE" || classification === "DIRECTIVE")
122
- return classification;
123
- return null;
124
- };
125
- /**
126
- * Factory class to build and compile the Intent Processing Graph.
127
- */
12
+ import { IntentGraphState } from "../domain/intent.state.js";
13
+ import { logger } from "./intent.graph.shared.js";
14
+ import { inferenceNode, prepNode } from "./intent.graph.infer.js";
15
+ import { reconciliationNode, verificationNode } from "./intent.graph.reconcile.js";
16
+ import { executorNode, queryNode } from "./intent.graph.execute.js";
17
+ export { buildExplicitUpdateActions, enforceIntentActionBoundary } from "./intent.graph.shared.js";
128
18
  export class IntentGraphFactory {
129
19
  constructor(database, embedder, intentQueue, questionerEnqueue, agents) {
130
- this.database = database;
131
- this.embedder = embedder;
132
- this.intentQueue = intentQueue;
133
- this.questionerEnqueue = questionerEnqueue;
134
- this.agents = agents;
20
+ this.deps = {
21
+ database,
22
+ embedder,
23
+ intentQueue,
24
+ questionerEnqueue,
25
+ inferrer: agents?.inferrer ?? new ExplicitIntentInferrer(),
26
+ verifier: agents?.verifier ?? new SemanticVerifier(),
27
+ reconciler: agents?.reconciler ?? new IntentReconciler(),
28
+ };
135
29
  }
136
30
  createGraph() {
137
- // Instantiate Agents (Nodes)
138
- const inferrer = this.agents?.inferrer ?? new ExplicitIntentInferrer();
139
- const verifier = this.agents?.verifier ?? new SemanticVerifier();
140
- const reconciler = this.agents?.reconciler ?? new IntentReconciler();
141
- // --- NODE DEFINITIONS ---
142
- /**
143
- * Node 0: Prep
144
- * Always fetches ALL of the user's active intents from the DB via getActiveIntents(userId).
145
- * This ensures reconciliation can detect duplicates and modifications globally,
146
- * regardless of network scope.
147
- */
148
- const prepNode = async (state) => {
149
- return timed("IntentGraph.prep", async () => {
150
- logger.verbose("Starting preparation phase", {
151
- operationMode: state.operationMode,
152
- hasContent: !!state.inputContent,
153
- targetIntentIds: state.targetIntentIds,
154
- networkId: state.networkId,
155
- });
156
- // Gate: write operations require an existing profile
157
- if (state.operationMode !== 'read') {
158
- const profile = await this.database.getProfile(state.userId);
159
- if (!profile) {
160
- const msg = "You need to create a profile before creating intents. Please set up your profile first.";
161
- logger.error("Prep failed: no profile for user", { userId: state.userId });
162
- return { error: msg };
163
- }
164
- }
165
- const activeIntents = await this.database.getActiveIntents(state.userId);
166
- const formattedActiveIntents = activeIntents
167
- .map(i => `ID: ${i.id}, Description: ${i.payload}, Summary: ${i.summary || 'N/A'}`)
168
- .join('\n') || "No active intents.";
169
- logger.verbose("Fetched active intents", {
170
- count: activeIntents.length,
171
- operationMode: state.operationMode
172
- });
173
- return {
174
- activeIntents: formattedActiveIntents,
175
- activeIntentIds: activeIntents.map((intent) => intent.id),
176
- trace: [{
177
- node: "prep",
178
- detail: `Fetched ${activeIntents.length} active intent(s)`,
179
- }],
180
- };
181
- });
182
- };
183
- /**
184
- * Node 1: Inference
185
- * Extracts intents from raw content.
186
- * Phase 4: Uses operation mode to control behavior and determine if node should execute.
187
- * Phase 5: Passes conversation context for anaphoric resolution.
188
- */
189
- const inferenceNode = async (state) => {
190
- return timed("IntentGraph.inference", async () => {
191
- logger.verbose("Starting inference", {
192
- operationMode: state.operationMode,
193
- hasContent: !!state.inputContent,
194
- contentPreview: state.inputContent?.substring(0, 50),
195
- hasConversationContext: !!state.conversationContext,
196
- conversationMessagesCount: state.conversationContext?.length || 0
197
- });
198
- const agentTimingsAccum = [];
199
- // Phase 4: Control profile fallback based on operation mode
200
- // Only allow for create operations without explicit content
201
- const allowProfileFallback = state.operationMode === 'create' && !state.inputContent;
202
- // Cast operationMode: 'read' and 'propose' map to 'create' for the inferrer
203
- // (inference node is never called in read mode; propose behaves like create for inference)
204
- const inferrerMode = (state.operationMode === 'read' || state.operationMode === 'propose') ? 'create' : state.operationMode;
205
- const _traceEmitterInferrer = requestContext.getStore()?.traceEmitter;
206
- const inferrerStart = Date.now();
207
- _traceEmitterInferrer?.({ type: "agent_start", name: "intent-inferrer" });
208
- const result = await inferrer.invoke(state.inputContent || null, state.userProfile, {
209
- allowProfileFallback,
210
- operationMode: inferrerMode,
211
- conversationContext: state.conversationContext // Phase 5: Pass conversation history
212
- });
213
- agentTimingsAccum.push({ name: 'intent.inferrer', durationMs: Date.now() - inferrerStart });
214
- _traceEmitterInferrer?.({ type: "agent_end", name: "intent-inferrer", durationMs: Date.now() - inferrerStart, summary: result.intents.length > 0 ? `Extracted ${result.intents.length} intent(s)` : "intent-inferrer completed" });
215
- logger.verbose("Inference complete", {
216
- inferredCount: result.intents.length,
217
- operationMode: state.operationMode
218
- });
219
- const descriptions = result.intents.map(i => i.description).slice(0, 3);
220
- const truncated = result.intents.length > 3 ? `... +${result.intents.length - 3} more` : "";
221
- return {
222
- inferredIntents: result.intents,
223
- agentTimings: agentTimingsAccum,
224
- trace: [{
225
- node: "inference",
226
- detail: result.intents.length === 0
227
- ? "No intents extracted"
228
- : `Extracted ${result.intents.length}: ${descriptions.map(d => `"${d.slice(0, 50)}${d.length > 50 ? '...' : ''}"`).join(", ")}${truncated}`,
229
- }],
230
- };
231
- });
232
- };
233
- /**
234
- * Node 2: Verification (Map-Reduce / Parallel)
235
- * Verifies each inferred intent in parallel.
236
- * Phase 4: Can be skipped for delete operations and updates with no new intents.
237
- */
238
- const verificationNode = async (state) => {
239
- return timed("IntentGraph.verification", async () => {
240
- const intents = state.inferredIntents;
241
- logger.verbose("Starting verification", {
242
- operationMode: state.operationMode,
243
- intentCount: intents.length
244
- });
245
- if (intents.length === 0) {
246
- logger.verbose("No intents to verify");
247
- return { verifiedIntents: [], agentTimings: [] };
248
- }
249
- logger.verbose('Verifying intents in parallel', { count: intents.length });
250
- const agentTimingsAccum = [];
251
- // Parallel Execution
252
- const verificationResults = await Promise.all(intents.map(async (intent) => {
253
- try {
254
- let description = intent.description;
255
- const _traceEmitterVerifier = requestContext.getStore()?.traceEmitter;
256
- const verifierStart1 = Date.now();
257
- _traceEmitterVerifier?.({ type: "agent_start", name: "intent-verifier" });
258
- let verdict = await verifier.invoke(description, state.userProfile);
259
- agentTimingsAccum.push({ name: 'intent.verifier', durationMs: Date.now() - verifierStart1 });
260
- _traceEmitterVerifier?.({ type: "agent_end", name: "intent-verifier", durationMs: Date.now() - verifierStart1, summary: `Verified: ${verdict.classification}` });
261
- if (isVague(description, verdict.semantic_entropy, verdict.felicity_scores.clarity)) {
262
- // Role-hint enrichment for vague job intents reads the global
263
- // user_context paragraph instead of the structured profile fields.
264
- const roleHintContext = (await this.database.getUserContext(state.userId, null))?.text ?? '';
265
- const enrichedDescription = enrichVagueIntentWithContext(description, roleHintContext);
266
- if (enrichedDescription !== description) {
267
- logger.verbose("Enriched vague intent using profile context", {
268
- before: description,
269
- after: enrichedDescription,
270
- });
271
- const _traceEmitterVerifier2 = requestContext.getStore()?.traceEmitter;
272
- const verifierStart2 = Date.now();
273
- _traceEmitterVerifier2?.({ type: "agent_start", name: "intent-verifier" });
274
- const enrichedVerdict = await verifier.invoke(enrichedDescription, state.userProfile);
275
- agentTimingsAccum.push({ name: 'intent.verifier', durationMs: Date.now() - verifierStart2 });
276
- _traceEmitterVerifier2?.({ type: "agent_end", name: "intent-verifier", durationMs: Date.now() - verifierStart2, summary: `Verified (enriched): ${enrichedVerdict.classification}` });
277
- const becameClear = enrichedVerdict.semantic_entropy < verdict.semantic_entropy ||
278
- enrichedVerdict.felicity_scores.clarity > verdict.felicity_scores.clarity;
279
- if (becameClear) {
280
- description = enrichedDescription;
281
- verdict = enrichedVerdict;
282
- }
283
- }
284
- }
285
- // Filter Logic: Must be a Commissive, Directive, or Declaration
286
- const VALID_TYPES = ['COMMISSIVE', 'DIRECTIVE', 'DECLARATION'];
287
- if (!VALID_TYPES.includes(verdict.classification)) {
288
- logger.warn('Dropping intent', { description, classification: verdict.classification });
289
- return {
290
- failure: {
291
- category: 'non_actionable',
292
- classification: verdict.classification,
293
- referentialBreadth: verdict.referential_breadth,
294
- message: `Description was classified as ${verdict.classification}, not an actionable goal.`,
295
- },
296
- };
297
- }
298
- if (isVague(description, verdict.semantic_entropy, verdict.felicity_scores.clarity)) {
299
- logger.warn('Dropping vague intent after verification', {
300
- description,
301
- entropy: verdict.semantic_entropy,
302
- clarity: verdict.felicity_scores.clarity,
303
- });
304
- return {
305
- failure: {
306
- category: 'vague_or_invalid',
307
- classification: verdict.classification,
308
- referentialBreadth: verdict.referential_breadth,
309
- message: 'Description failed clarity or semantic-entropy requirements.',
310
- },
311
- };
312
- }
313
- if (state.operationMode === 'create' && verdict.referential_breadth === 'broad') {
314
- logger.warn('Dropping broad attributive intent before persistence', {
315
- description,
316
- referentialBreadth: verdict.referential_breadth,
317
- missingSelectionalConstraints: verdict.missing_selectional_constraints,
318
- warning: getSpecificityWarning(verdict),
319
- });
320
- return {
321
- failure: {
322
- category: 'vague_or_invalid',
323
- classification: verdict.classification,
324
- referentialBreadth: verdict.referential_breadth,
325
- message: getSpecificityWarning(verdict),
326
- },
327
- };
328
- }
329
- // Calculate Score
330
- const score = Math.min(verdict.felicity_scores.authority, verdict.felicity_scores.sincerity, verdict.felicity_scores.clarity);
331
- // Return enriched intent
332
- return {
333
- intent: {
334
- ...intent,
335
- description,
336
- verification: verdict,
337
- score,
338
- },
339
- };
340
- }
341
- catch (e) {
342
- logger.error('Error verifying intent', { description: intent.description, error: e });
343
- return {
344
- failure: {
345
- category: 'verification_failure',
346
- message: e instanceof Error ? e.message : 'Intent verification failed unexpectedly.',
347
- },
348
- };
349
- }
350
- }));
351
- const verified = verificationResults.flatMap((result) => 'intent' in result ? [result.intent] : []);
352
- const validationFailures = verificationResults.flatMap((result) => 'failure' in result ? [result.failure] : []);
353
- logger.verbose(`Verification complete`, {
354
- passed: verified.length,
355
- total: intents.length,
356
- operationMode: state.operationMode
357
- });
358
- // Build trace entries with Felicity scores for each verified intent
359
- const traceEntries = verified.map(v => {
360
- const fs = v.verification?.felicity_scores;
361
- const entropy = v.verification?.semantic_entropy;
362
- const classification = v.verification?.classification;
363
- const referentialBreadth = v.verification?.referential_breadth;
364
- return {
365
- node: "verification",
366
- detail: `"${v.description.slice(0, 40)}${v.description.length > 40 ? '...' : ''}" → ${classification}${referentialBreadth ? ` (${referentialBreadth} referential breadth)` : ''}`,
367
- data: fs ? {
368
- clarity: fs.clarity,
369
- authority: fs.authority,
370
- sincerity: fs.sincerity,
371
- entropy: entropy != null ? Math.round(entropy * 100) / 100 : undefined,
372
- referentialBreadth,
373
- missingSelectionalConstraints: v.verification?.missing_selectional_constraints,
374
- specificityWarning: v.verification?.specificity_warning ?? undefined,
375
- classification,
376
- score: v.score,
377
- } : undefined,
378
- };
379
- });
380
- // Add summary trace if some intents were filtered out
381
- const dropped = intents.length - verified.length;
382
- if (dropped > 0) {
383
- traceEntries.unshift({
384
- node: "verification",
385
- detail: `Verified ${verified.length}/${intents.length} (${dropped} filtered as invalid)`,
386
- data: undefined,
387
- });
388
- }
389
- else if (verified.length > 0) {
390
- traceEntries.unshift({
391
- node: "verification",
392
- detail: `Verified ${verified.length} intent(s)`,
393
- data: undefined,
394
- });
395
- }
396
- return { verifiedIntents: verified, validationFailures, agentTimings: agentTimingsAccum, trace: traceEntries };
397
- });
398
- };
399
- /**
400
- * Node 3: Reconciliation
401
- * Decides on final actions (Create, Update, Expire).
402
- * Phase 4: Handles delete operations directly without LLM reconciliation.
403
- */
404
- const reconciliationNode = async (state) => {
405
- return timed("IntentGraph.reconciliation", async () => {
406
- logger.verbose("Starting reconciliation", {
407
- operationMode: state.operationMode,
408
- verifiedIntentCount: state.verifiedIntents.length,
409
- targetIntentIds: state.targetIntentIds
410
- });
411
- const agentTimingsAccum = [];
412
- // Phase 4: Handle delete operations directly
413
- if (state.operationMode === 'delete') {
414
- if (!state.targetIntentIds || state.targetIntentIds.length === 0) {
415
- logger.warn("Delete mode with no target IDs");
416
- return {
417
- actions: [],
418
- agentTimings: agentTimingsAccum,
419
- trace: [{ node: "reconciler", detail: "Delete mode with no target IDs" }],
420
- };
421
- }
422
- logger.verbose("Delete mode - generating expire actions", {
423
- targetIds: state.targetIntentIds
424
- });
425
- const actions = state.targetIntentIds.map(id => ({
426
- type: 'expire',
427
- id,
428
- reasoning: 'User requested deletion'
429
- }));
430
- return {
431
- actions,
432
- agentTimings: agentTimingsAccum,
433
- trace: [{
434
- node: "reconciler",
435
- detail: `Actions: expire=${actions.length}`,
436
- }],
437
- };
438
- }
439
- // Standard reconciliation for create/update operations
440
- const candidates = state.verifiedIntents;
441
- if (candidates.length === 0) {
442
- logger.verbose("No verified intents to reconcile");
443
- return {
444
- actions: [],
445
- agentTimings: agentTimingsAccum,
446
- trace: [{ node: "reconciler", detail: "No intents to reconcile" }],
447
- };
448
- }
449
- if (state.operationMode === 'update') {
450
- const explicitUpdate = buildExplicitUpdateActions(state.targetIntentIds, state.activeIntentIds, candidates);
451
- return {
452
- actions: explicitUpdate.actions,
453
- validationFailures: explicitUpdate.failure
454
- ? [...state.validationFailures, explicitUpdate.failure]
455
- : state.validationFailures,
456
- agentTimings: agentTimingsAccum,
457
- trace: [{
458
- node: 'reconciler',
459
- detail: explicitUpdate.failure
460
- ? `Explicit update rejected: ${explicitUpdate.failure.category}`
461
- : `Explicit update bound to target ${state.targetIntentIds?.[0]}`,
462
- }],
463
- };
464
- }
465
- // Format candidates for the Reconciler Prompt
466
- const formattedCandidates = candidates.map(c => `- [${c.type.toUpperCase()}] "${c.description}" (Confidence: ${c.confidence}, Score: ${c.score})\n` +
467
- ` Reasoning: ${c.reasoning}\n` +
468
- ` Verification: ${c.verification?.classification} (Flags: ${c.verification?.flags.join(', ') || 'None'})`).join('\n');
469
- logger.verbose("Invoking reconciler agent", {
470
- candidateCount: candidates.length,
471
- operationMode: state.operationMode
472
- });
473
- const _traceEmitterReconciler = requestContext.getStore()?.traceEmitter;
474
- const reconcilerStart = Date.now();
475
- _traceEmitterReconciler?.({ type: "agent_start", name: "intent-reconciler" });
476
- const result = await reconciler.invoke(formattedCandidates, state.activeIntents);
477
- agentTimingsAccum.push({ name: 'intent.reconciler', durationMs: Date.now() - reconcilerStart });
478
- _traceEmitterReconciler?.({ type: "agent_end", name: "intent-reconciler", durationMs: Date.now() - reconcilerStart, summary: `Reconciled ${result.actions.length} action(s)` });
479
- const actions = enforceIntentActionBoundary(state.operationMode, state.targetIntentIds, result.actions);
480
- logger.verbose("Reconciliation complete", {
481
- actionCount: actions.length,
482
- droppedActionCount: result.actions.length - actions.length,
483
- operationMode: state.operationMode
484
- });
485
- // Count actions by type after enforcing the operation boundary.
486
- const counts = { create: 0, update: 0, expire: 0 };
487
- for (const a of actions) {
488
- if (a.type in counts)
489
- counts[a.type]++;
490
- }
491
- return {
492
- actions,
493
- agentTimings: agentTimingsAccum,
494
- trace: [{
495
- node: "reconciler",
496
- detail: `Actions: create=${counts.create}, update=${counts.update}, expire=${counts.expire}`,
497
- }],
498
- };
499
- });
500
- };
501
- /**
502
- * Generate a flat embedding for an intent payload, swallowing failures so
503
- * persistence can continue without an embedding. `intentId` is logging-only
504
- * (present for updates, absent for creates).
505
- */
506
- const generateIntentEmbedding = async (sanitizedPayload, intentId) => {
507
- if (!this.embedder)
508
- return undefined;
509
- try {
510
- const embedding = await this.embedder.generate(sanitizedPayload, undefined, getAbortSignalConfig());
511
- const flatEmbedding = Array.isArray(embedding?.[0])
512
- ? embedding[0]
513
- : embedding;
514
- logger.verbose("Generated embedding for intent", {
515
- ...(intentId ? { intentId } : {}),
516
- dimensions: flatEmbedding?.length,
517
- });
518
- return flatEmbedding;
519
- }
520
- catch (embErr) {
521
- logger.error("Failed to generate embedding for intent (continuing without)", {
522
- ...(intentId ? { intentId } : {}),
523
- error: embErr,
524
- });
525
- return undefined;
526
- }
527
- };
528
- /**
529
- * Node 4: Executor
530
- * Executes reconciler actions against the database.
531
- */
532
- const executorNode = async (state) => {
533
- return timed("IntentGraph.executor", async () => {
534
- const actions = enforceIntentActionBoundary(state.operationMode, state.targetIntentIds, state.actions ?? []);
535
- if (actions.length === 0) {
536
- return { executionResults: [] };
537
- }
538
- logger.verbose('Executing actions', { count: actions.length });
539
- const results = [];
540
- const scopeEnvelope = state.scopeType && state.scopeId
541
- ? { scopeType: state.scopeType, scopeId: state.scopeId }
542
- : {};
543
- const verifiedIntentByPayload = new Map();
544
- for (const verifiedIntent of state.verifiedIntents) {
545
- verifiedIntentByPayload.set(verifiedIntent.description, verifiedIntent);
546
- verifiedIntentByPayload.set(normalizeIntentDescription(verifiedIntent.description), verifiedIntent);
547
- }
548
- for (const action of actions) {
549
- const actionType = action.type.toLowerCase();
550
- try {
551
- if (actionType === 'create') {
552
- const createAction = action;
553
- const sanitizedPayload = normalizeIntentDescription(createAction.payload);
554
- const matchedVerifiedIntent = verifiedIntentByPayload.get(createAction.payload) ||
555
- verifiedIntentByPayload.get(sanitizedPayload);
556
- // Generate embedding for the intent payload
557
- const flatEmbedding = await generateIntentEmbedding(sanitizedPayload);
558
- const created = await this.database.createIntent({
559
- userId: state.userId,
560
- payload: sanitizedPayload,
561
- confidence: createAction.score ? createAction.score / 100 : 1.0,
562
- inferenceType: 'explicit',
563
- sourceType: 'discovery_form',
564
- embedding: flatEmbedding,
565
- semanticEntropy: createAction.semanticEntropy ??
566
- matchedVerifiedIntent?.verification?.semantic_entropy ??
567
- null,
568
- referentialAnchor: createAction.referentialAnchor ??
569
- matchedVerifiedIntent?.verification?.referential_anchor ??
570
- null,
571
- felicityAuthority: matchedVerifiedIntent?.verification?.felicity_scores.authority ?? null,
572
- felicitySincerity: matchedVerifiedIntent?.verification?.felicity_scores.sincerity ?? null,
573
- felicityClarity: matchedVerifiedIntent?.verification?.felicity_scores.clarity ?? null,
574
- intentMode: createAction.intentMode ?? null,
575
- speechActType: toSpeechActType(matchedVerifiedIntent?.verification?.classification),
576
- });
577
- results.push({ actionType: 'create', success: true, intentId: created.id, payload: sanitizedPayload });
578
- logger.verbose('Created intent', { intentId: created.id });
579
- this.intentQueue?.addGenerateHydeJob({
580
- intentId: created.id,
581
- userId: state.userId,
582
- ...scopeEnvelope,
583
- }).catch((err) => logger.error('Failed to enqueue intent HyDE job', { intentId: created.id, error: err }));
584
- if (this.questionerEnqueue) {
585
- const userContext = (await this.database.getUserContext(state.userId, null))?.text ?? '';
586
- this.questionerEnqueue({
587
- mode: 'intent',
588
- userId: state.userId,
589
- sourceType: 'intent',
590
- sourceId: created.id,
591
- ...scopeEnvelope,
592
- context: {
593
- intentId: created.id,
594
- payload: sanitizedPayload,
595
- userContext,
596
- },
597
- }).catch((err) => logger.error('Failed to enqueue intent question generation', { intentId: created.id, error: err }));
598
- }
599
- }
600
- else if (actionType === 'update') {
601
- const updateAction = action;
602
- const sanitizedPayload = normalizeIntentDescription(updateAction.payload);
603
- const matchedVerifiedIntent = verifiedIntentByPayload.get(updateAction.payload) ||
604
- verifiedIntentByPayload.get(sanitizedPayload);
605
- // Regenerate embedding for the updated payload
606
- const flatEmbedding = await generateIntentEmbedding(sanitizedPayload, updateAction.id);
607
- const updated = await this.database.updateIntent(updateAction.id, {
608
- payload: sanitizedPayload,
609
- embedding: flatEmbedding,
610
- semanticEntropy: matchedVerifiedIntent?.verification?.semantic_entropy ??
611
- null,
612
- referentialAnchor: matchedVerifiedIntent?.verification?.referential_anchor ??
613
- null,
614
- felicityAuthority: matchedVerifiedIntent?.verification?.felicity_scores.authority ?? null,
615
- felicitySincerity: matchedVerifiedIntent?.verification?.felicity_scores.sincerity ?? null,
616
- felicityClarity: matchedVerifiedIntent?.verification?.felicity_scores.clarity ?? null,
617
- intentMode: updateAction.intentMode ?? null,
618
- speechActType: toSpeechActType(matchedVerifiedIntent?.verification?.classification),
619
- ...(state.expectedIntentFingerprint !== undefined ? {
620
- expectedIntentFingerprint: state.expectedIntentFingerprint,
621
- expectedIntentUserId: state.userId,
622
- } : {}),
623
- });
624
- results.push({
625
- actionType: 'update',
626
- success: !!updated,
627
- intentId: updateAction.id,
628
- payload: sanitizedPayload,
629
- error: updated ? undefined : 'Intent not found'
630
- });
631
- logger.verbose('Updated intent', { intentId: updateAction.id });
632
- if (updated) {
633
- this.intentQueue?.addGenerateHydeJob({
634
- intentId: updateAction.id,
635
- userId: state.userId,
636
- ...scopeEnvelope,
637
- }).catch((err) => logger.error('Failed to enqueue intent HyDE job', { intentId: updateAction.id, error: err }));
638
- }
639
- }
640
- else if (actionType === 'expire') {
641
- const expireAction = action;
642
- const result = await this.database.archiveIntent(expireAction.id);
643
- results.push({
644
- actionType: 'expire',
645
- success: result.success,
646
- intentId: expireAction.id,
647
- error: result.error
648
- });
649
- logger.verbose('Archived intent', { intentId: expireAction.id });
650
- if (result.success) {
651
- this.intentQueue?.addDeleteHydeJob({ intentId: expireAction.id }).catch((err) => logger.error('Failed to enqueue intent HyDE delete job', { intentId: expireAction.id, error: err }));
652
- }
653
- }
654
- }
655
- catch (error) {
656
- logger.error('Failed to execute action', { actionType: action.type, error });
657
- results.push({
658
- actionType,
659
- success: false,
660
- intentId: 'id' in action ? action.id : undefined,
661
- error: error instanceof Error ? error.message : 'Unknown error'
662
- });
663
- }
664
- }
665
- return { executionResults: results };
666
- });
667
- };
668
- // --- QUERY NODE (Read Mode) ---
669
- /**
670
- * Node: Query
671
- * Fast-path read node — fetches intents from DB based on scope.
672
- * Handles: global user intents, network-scoped (all or filtered by user).
673
- * No LLM calls; no inference/verification/reconciliation.
674
- */
675
- const queryNode = async (state) => {
676
- return timed("IntentGraph.query", async () => {
677
- logger.verbose("Starting query (read mode)", {
678
- userId: state.userId,
679
- networkId: state.networkId,
680
- queryUserId: state.queryUserId,
681
- allUserIntents: state.allUserIntents,
682
- });
683
- try {
684
- // Scope-aware default: caller's intents across all reachable networks.
685
- // Triggered when the tool layer passed indexScope and did not pick a
686
- // specific networkId or queryUserId — i.e. "my intents" in a chat
687
- // where the agent's reach is more than one index.
688
- if (!state.queryUserId &&
689
- !state.networkId &&
690
- state.indexScope &&
691
- state.indexScope.length > 0) {
692
- const intents = await this.database.getActiveIntentsAcrossIndexes(state.userId, state.indexScope);
693
- if (intents.length === 0) {
694
- return {
695
- readResult: {
696
- count: 0,
697
- intents: [],
698
- message: "You don't have any active intents yet. Share what you're looking for.",
699
- },
700
- };
701
- }
702
- return {
703
- readResult: {
704
- count: intents.length,
705
- intents: intents.map((i) => ({
706
- id: i.id,
707
- description: i.payload,
708
- summary: i.summary,
709
- createdAt: i.createdAt,
710
- })),
711
- },
712
- };
713
- }
714
- // When allUserIntents is true, ignore network scope and return all
715
- const effectiveIndexId = state.allUserIntents ? undefined : state.networkId;
716
- if (effectiveIndexId) {
717
- // Verify membership
718
- const isMember = await this.database.isNetworkMember(effectiveIndexId, state.userId);
719
- if (!isMember) {
720
- return {
721
- readResult: {
722
- count: 0,
723
- intents: [],
724
- message: "Index not found or you are not a member.",
725
- },
726
- };
727
- }
728
- // Network-scoped read
729
- if (!state.queryUserId) {
730
- // All intents in the index (any member can see)
731
- const intents = await this.database.getNetworkIntentsForMember(effectiveIndexId, state.userId, { limit: 50, offset: 0 });
732
- if (intents.length === 0) {
733
- return {
734
- readResult: {
735
- count: 0,
736
- intents: [],
737
- message: "No intents in this network yet.",
738
- networkId: effectiveIndexId,
739
- },
740
- };
741
- }
742
- return {
743
- readResult: {
744
- count: intents.length,
745
- networkId: effectiveIndexId,
746
- intents: intents.map((i) => ({
747
- id: i.id,
748
- description: i.payload,
749
- summary: i.summary,
750
- createdAt: i.createdAt,
751
- userId: i.userId,
752
- userName: i.userName,
753
- })),
754
- },
755
- };
756
- }
757
- // Specific user's intents in the index
758
- const effectiveUserId = state.queryUserId;
759
- const intents = await this.database.getIntentsInIndexForMember(effectiveUserId, effectiveIndexId);
760
- if (intents.length === 0) {
761
- return {
762
- readResult: {
763
- count: 0,
764
- intents: [],
765
- message: effectiveUserId === state.userId
766
- ? "You don't have any intents in this network yet."
767
- : "No intents for that user in this network.",
768
- networkId: effectiveIndexId,
769
- },
770
- };
771
- }
772
- const user = await this.database.getUser(effectiveUserId);
773
- const userName = user?.name ?? null;
774
- return {
775
- readResult: {
776
- count: intents.length,
777
- networkId: effectiveIndexId,
778
- intents: intents.map((i) => ({
779
- id: i.id,
780
- description: i.payload,
781
- summary: i.summary,
782
- createdAt: i.createdAt,
783
- userId: effectiveUserId,
784
- userName,
785
- })),
786
- },
787
- };
788
- }
789
- // Global (no network scope): return user's own active intents
790
- const intents = await this.database.getActiveIntents(state.userId);
791
- if (intents.length === 0) {
792
- return {
793
- readResult: {
794
- count: 0,
795
- intents: [],
796
- message: "You don't have any active intents yet. Share what you're looking for.",
797
- },
798
- };
799
- }
800
- return {
801
- readResult: {
802
- count: intents.length,
803
- intents: intents.map((i) => ({
804
- id: i.id,
805
- description: i.payload,
806
- summary: i.summary,
807
- createdAt: i.createdAt,
808
- })),
809
- },
810
- };
811
- }
812
- catch (err) {
813
- logger.error("Query node failed", { error: err });
814
- return {
815
- readResult: {
816
- count: 0,
817
- intents: [],
818
- message: "Failed to fetch intents. Please try again.",
819
- },
820
- };
821
- }
822
- });
823
- };
824
- // --- CONDITIONAL ROUTING FUNCTIONS ---
825
- /**
826
- * After prep: read mode → query; otherwise decide inference vs reconciler by operation mode.
827
- */
828
- const afterPrepRoute = (state) => {
829
- if (state.error) {
830
- logger.warn('Prep failed with error, short-circuiting to END', { error: state.error });
831
- return '__end__';
832
- }
833
- if (state.operationMode === 'read') {
834
- logger.verbose('Read mode - routing to query (fast path)');
835
- return 'query';
836
- }
837
- return shouldRunInference(state);
838
- };
839
- /**
840
- * Determines if inference should run based on operation mode.
841
- * Delete operations skip inference entirely and go straight to reconciliation.
842
- */
843
- const shouldRunInference = (state) => {
844
- if (state.operationMode === 'delete') {
845
- logger.verbose('Delete mode - skipping inference, routing to reconciliation');
846
- return 'reconciler';
847
- }
848
- logger.verbose('Running inference', {
849
- operationMode: state.operationMode
850
- });
851
- return 'inference';
852
- };
853
- /**
854
- * Determines if verification should run based on operation mode and inferred intents.
855
- * Skips verification for:
856
- * - Operations with no inferred intents
857
- * - Can be extended to skip for update operations with no new intents
858
- */
859
- const shouldRunVerification = (state) => {
860
- if (state.inferredIntents.length === 0) {
861
- if (state.operationMode === 'propose') {
862
- logger.verbose('Propose mode with no inferred intents - exiting early');
863
- return '__end__';
864
- }
865
- logger.verbose('No intents to verify - skipping verification, routing to reconciliation');
866
- return 'reconciler';
867
- }
868
- if (state.operationMode === 'update') {
869
- logger.verbose('Update mode with new intents - running verification');
870
- return 'verification';
871
- }
872
- if (state.operationMode === 'create') {
873
- logger.verbose('Create mode - running verification');
874
- return 'verification';
875
- }
876
- // Default to verification for safety
877
- logger.verbose('Default routing to verification');
878
- return 'verification';
879
- };
880
- // --- GRAPH ASSEMBLY WITH CONDITIONAL EDGES (PHASE 4) ---
881
- const workflow = new StateGraph(IntentGraphState)
882
- .addNode("prep", prepNode)
883
- .addNode("query", queryNode)
884
- .addNode("inference", inferenceNode)
885
- .addNode("verification", verificationNode)
886
- .addNode("reconciler", reconciliationNode)
887
- .addNode("executor", executorNode)
31
+ const deps = this.deps;
32
+ return new StateGraph(IntentGraphState)
33
+ .addNode("prep", (state) => prepNode(state, deps))
34
+ .addNode("query", (state) => queryNode(state, deps))
35
+ .addNode("inference", (state) => inferenceNode(state, deps))
36
+ .addNode("verification", (state) => verificationNode(state, deps))
37
+ .addNode("reconciler", (state) => reconciliationNode(state, deps))
38
+ .addNode("executor", (state) => executorNode(state, deps))
888
39
  // Flow paths:
889
40
  // - READ: prep → query → END (fast path, no LLM calls)
890
41
  // - CREATE: prep → inference → verification → reconciler → executor → END
@@ -908,20 +59,77 @@ export class IntentGraphFactory {
908
59
  __end__: END,
909
60
  })
910
61
  // After verification: propose mode exits early; others continue to reconciliation
911
- .addConditionalEdges("verification", (state) => {
912
- if (state.operationMode === 'propose') {
913
- logger.verbose('Propose mode - stopping after verification, skipping reconciliation');
914
- return '__end__';
915
- }
916
- return 'reconciler';
917
- }, {
62
+ .addConditionalEdges("verification", routeAfterVerification, {
918
63
  reconciler: "reconciler",
919
64
  __end__: END,
920
65
  })
921
66
  // Reconciliation always goes to executor
922
67
  .addEdge("reconciler", "executor")
923
68
  // Executor is always the end
924
- .addEdge("executor", END);
925
- return workflow.compile();
69
+ .addEdge("executor", END)
70
+ .compile();
71
+ }
72
+ }
73
+ /**
74
+ * After prep: read mode → query; otherwise decide inference vs reconciler by operation mode.
75
+ */
76
+ export function afterPrepRoute(state) {
77
+ if (state.error) {
78
+ logger.warn('Prep failed with error, short-circuiting to END', { error: state.error });
79
+ return '__end__';
80
+ }
81
+ if (state.operationMode === 'read') {
82
+ logger.verbose('Read mode - routing to query (fast path)');
83
+ return 'query';
84
+ }
85
+ return shouldRunInference(state);
86
+ }
87
+ /**
88
+ * Determines if inference should run based on operation mode.
89
+ * Delete operations skip inference entirely and go straight to reconciliation.
90
+ */
91
+ export function shouldRunInference(state) {
92
+ if (state.operationMode === 'delete') {
93
+ logger.verbose('Delete mode - skipping inference, routing to reconciliation');
94
+ return 'reconciler';
95
+ }
96
+ logger.verbose('Running inference', {
97
+ operationMode: state.operationMode
98
+ });
99
+ return 'inference';
100
+ }
101
+ /**
102
+ * Determines if verification should run based on operation mode and inferred intents.
103
+ * Skips verification for:
104
+ * - Operations with no inferred intents
105
+ * - Can be extended to skip for update operations with no new intents
106
+ */
107
+ export function shouldRunVerification(state) {
108
+ if (state.inferredIntents.length === 0) {
109
+ if (state.operationMode === 'propose') {
110
+ logger.verbose('Propose mode with no inferred intents - exiting early');
111
+ return '__end__';
112
+ }
113
+ logger.verbose('No intents to verify - skipping verification, routing to reconciliation');
114
+ return 'reconciler';
115
+ }
116
+ if (state.operationMode === 'update') {
117
+ logger.verbose('Update mode with new intents - running verification');
118
+ return 'verification';
119
+ }
120
+ if (state.operationMode === 'create') {
121
+ logger.verbose('Create mode - running verification');
122
+ return 'verification';
123
+ }
124
+ // Default to verification for safety
125
+ logger.verbose('Default routing to verification');
126
+ return 'verification';
127
+ }
128
+ /** After verification: propose mode exits early; others continue to reconciliation. */
129
+ export function routeAfterVerification(state) {
130
+ if (state.operationMode === 'propose') {
131
+ logger.verbose('Propose mode - stopping after verification, skipping reconciliation');
132
+ return '__end__';
926
133
  }
134
+ return 'reconciler';
927
135
  }