@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
@@ -6,413 +6,364 @@ import { requestContext } from "../../shared/observability/request-context.js";
6
6
  import { renderNetworkContext } from "../../shared/network/metadata.renderer.js";
7
7
  import { IntentNetworkGraphState } from "./indexer.state.js";
8
8
  const logger = protocolLogger("IntentNetworkGraphFactory");
9
- /**
10
- * Factory class to build and compile the Intent–Network (indexer) Graph.
11
- *
12
- * Handles CRUD for the intent_indexes junction table:
13
- * - create: Assign an intent to a network (direct or LLM-evaluated)
14
- * - read: List intent–network links (by intentId or by networkId)
15
- * - delete: Unassign an intent from a network
16
- *
17
- * ## Signal assignment policy
18
- *
19
- * The `IntentIndexer` is injected at construction time from the signals public
20
- * facade via the communities ports layer — this factory never imports signals
21
- * application internals directly.
22
- *
23
- * Two assignment paths:
24
- * 1. Direct (`skipEvaluation: true`):
25
- * - Writes the link immediately with `mode: manual_override`, `score: 1`.
26
- * 2. Evaluated (`skipEvaluation: false`):
27
- * - Loads intent + network context (indexPrompt, memberPrompt).
28
- * - No-prompt fast path: if both prompts are absent, assigns with
29
- * `mode: automatic, promptPresence: 'none'` without calling the LLM.
30
- * - Otherwise invokes IntentIndexer to get indexScore + memberScore + reasoning.
31
- * - Applies `buildNetworkAssignmentDecision` to produce the threshold/metadata.
32
- * - Only writes the link when the decision's `assigned` flag is true.
33
- *
34
- * ## Membership authority for assignment
35
- *
36
- * A user may only assign their own intents.
37
- * The user must be a member (or owner) of the target network.
38
- * Assignment is rejected if membership has been revoked between intent creation
39
- * and assignment time (`assignIntentToNetworkIfMember` returns `membership_required`).
40
- *
41
- * Flow:
42
- * START → router → {
43
- * create: assignNode (direct or evaluated) → END
44
- * read: readNode → END
45
- * delete: unassignNode → END
46
- * }
47
- *
48
- * IND-546: canonical home — previously network/indexer/indexer.graph.ts.
49
- * Signals dependency consumed via capabilities/signals.facade.ts (through ports).
50
- */
51
9
  export class IntentNetworkGraphFactory {
52
10
  constructor(database, intentNetworker) {
53
- this.database = database;
54
- this.intentNetworker = intentNetworker;
11
+ this.deps = { database, intentNetworker };
55
12
  }
56
13
  createGraph() {
57
- const indexer = this.intentNetworker;
58
- const finalizeAssignment = async (userId, intentId, networkId, score, metadata, successMessage) => {
59
- const outcome = await this.database.assignIntentToNetworkIfMember(userId, intentId, networkId, score, metadata);
60
- if (outcome.kind === 'assigned') {
14
+ const deps = this.deps;
15
+ // --- GRAPH ASSEMBLY ---
16
+ const workflow = new StateGraph(IntentNetworkGraphState)
17
+ .addNode("assign", (state) => assignNode(state, deps))
18
+ .addNode("read", (state) => readNode(state, deps))
19
+ .addNode("unassign", (state) => unassignNode(state, deps))
20
+ .addConditionalEdges(START, routeByMode, {
21
+ assign: "assign",
22
+ read: "read",
23
+ unassign: "unassign",
24
+ })
25
+ .addEdge("assign", END)
26
+ .addEdge("read", END)
27
+ .addEdge("unassign", END);
28
+ return workflow.compile();
29
+ }
30
+ }
31
+ /**
32
+ * Persist an intent→network assignment, honouring membership, and shape the tool result.
33
+ */
34
+ export async function finalizeAssignment(deps, userId, intentId, networkId, score, metadata, successMessage) {
35
+ const outcome = await deps.database.assignIntentToNetworkIfMember(userId, intentId, networkId, score, metadata);
36
+ if (outcome.kind === 'assigned') {
37
+ return {
38
+ assignmentResult: { networkId, assigned: true, success: true },
39
+ mutationResult: { success: true, message: successMessage },
40
+ };
41
+ }
42
+ if (outcome.kind === 'already_assigned') {
43
+ return {
44
+ assignmentResult: { networkId, assigned: true, success: true },
45
+ mutationResult: { success: true, message: 'That intent is already in this network.' },
46
+ };
47
+ }
48
+ if (outcome.kind === 'membership_required') {
49
+ return {
50
+ assignmentResult: { networkId, assigned: false, success: false },
51
+ mutationResult: { success: false, error: 'Current network membership is required to assign this intent.' },
52
+ };
53
+ }
54
+ return {
55
+ assignmentResult: { networkId, assigned: false, success: false },
56
+ mutationResult: { success: false, error: 'Intent is no longer available for assignment.' },
57
+ };
58
+ }
59
+ export async function assignNode(state, deps) {
60
+ return timed("IntentNetworkGraph.assign", async () => {
61
+ const intentId = state.intentId;
62
+ const networkId = state.networkId;
63
+ logger.verbose("Assign intent to network", { userId: state.userId, intentId, networkId, skipEvaluation: state.skipEvaluation });
64
+ const agentTimingsAccum = [];
65
+ if (!intentId || !networkId) {
66
+ return { agentTimings: agentTimingsAccum, mutationResult: { success: false, error: "Both intentId and networkId are required." } };
67
+ }
68
+ try {
69
+ // Validate ownership and membership
70
+ const intent = await deps.database.getIntent(intentId);
71
+ if (!intent) {
72
+ return { agentTimings: agentTimingsAccum, mutationResult: { success: false, error: "Intent not found." } };
73
+ }
74
+ if (intent.userId !== state.userId) {
75
+ return { agentTimings: agentTimingsAccum, mutationResult: { success: false, error: "You can only add your own intents to a network." } };
76
+ }
77
+ const [isMember, isOwner] = await Promise.all([
78
+ deps.database.isNetworkMember(networkId, state.userId),
79
+ deps.database.isIndexOwner(networkId, state.userId),
80
+ ]);
81
+ if (!isMember && !isOwner) {
82
+ return { agentTimings: agentTimingsAccum, mutationResult: { success: false, error: "You are not a member of that network." } };
83
+ }
84
+ // Check if already assigned
85
+ const alreadyAssigned = await deps.database.isIntentAssignedToIndex(intentId, networkId);
86
+ if (alreadyAssigned) {
87
+ return { agentTimings: agentTimingsAccum, mutationResult: { success: true, message: "That intent is already in this network." } };
88
+ }
89
+ // Direct assignment (skip LLM evaluation)
90
+ if (state.skipEvaluation) {
91
+ const decision = buildNetworkAssignmentDecision({
92
+ resourceType: "intent",
93
+ mode: "manual_override",
94
+ scope: "network",
95
+ evaluator: "intent-network-graph",
96
+ source: "manual-index-assignment",
97
+ createdAt: new Date().toISOString(),
98
+ });
99
+ const finalized = await finalizeAssignment(deps, state.userId, intentId, networkId, decision.finalScore, decision.metadata, 'Intent saved to the network.');
100
+ return { agentTimings: agentTimingsAccum, ...finalized };
101
+ }
102
+ // Evaluated assignment path
103
+ const intentForIndexing = await deps.database.getIntentForIndexing(intentId);
104
+ if (!intentForIndexing) {
105
+ return { agentTimings: agentTimingsAccum, mutationResult: { success: false, error: "Intent not found for evaluation." } };
106
+ }
107
+ const indexContext = await deps.database.getNetworkAssignmentContext(networkId, intentForIndexing.userId);
108
+ if (!indexContext) {
61
109
  return {
62
- assignmentResult: { networkId, assigned: true, success: true },
63
- mutationResult: { success: true, message: successMessage },
110
+ agentTimings: agentTimingsAccum,
111
+ assignmentResult: { networkId, assigned: false, success: false },
112
+ mutationResult: { success: false, error: "Network assignment context not found." },
64
113
  };
65
114
  }
66
- if (outcome.kind === 'already_assigned') {
115
+ const indexPrompt = indexContext.indexPrompt ?? null;
116
+ const memberPrompt = indexContext.memberPrompt ?? null;
117
+ const hasNoPrompts = !indexPrompt?.trim() && !memberPrompt?.trim();
118
+ // No-prompt fast path: assign without LLM
119
+ if (hasNoPrompts) {
120
+ const decision = buildNetworkAssignmentDecision({
121
+ resourceType: "intent",
122
+ mode: "automatic",
123
+ scope: "network",
124
+ indexPrompt,
125
+ memberPrompt,
126
+ evaluator: "intent-networker",
127
+ source: "intent-network-graph",
128
+ createdAt: new Date().toISOString(),
129
+ });
130
+ const finalized = await finalizeAssignment(deps, state.userId, intentId, networkId, decision.finalScore, decision.metadata, 'Intent assigned to network (no prompts).');
131
+ return { agentTimings: agentTimingsAccum, ...finalized };
132
+ }
133
+ // Render network context for the evaluator
134
+ const network = await deps.database.getNetwork(networkId);
135
+ const renderedContext = network
136
+ ? renderNetworkContext({
137
+ title: network.title,
138
+ prompt: network.prompt,
139
+ })
140
+ : null;
141
+ const sourceName = intentForIndexing.sourceType
142
+ ? `${intentForIndexing.sourceType}:${intentForIndexing.sourceId ?? ""}`
143
+ : undefined;
144
+ // Run IntentIndexer evaluation (injected from signals public facade via ports)
145
+ const _traceEmitter = requestContext.getStore()?.traceEmitter;
146
+ const _indexerStart = Date.now();
147
+ _traceEmitter?.({ type: "agent_start", name: "intent-networker" });
148
+ let result = null;
149
+ try {
150
+ result = await deps.intentNetworker.evaluate(intentForIndexing.payload, indexPrompt, memberPrompt, sourceName, renderedContext);
151
+ }
152
+ finally {
153
+ const _indexerMs = Date.now() - _indexerStart;
154
+ agentTimingsAccum.push({ name: 'intent.indexer', durationMs: _indexerMs });
155
+ _traceEmitter?.({
156
+ type: "agent_end",
157
+ name: "intent-networker",
158
+ durationMs: _indexerMs,
159
+ summary: result
160
+ ? `Scored: index=${result.indexScore.toFixed(2)}, member=${result.memberScore.toFixed(2)}`
161
+ : "intent-networker failed",
162
+ });
163
+ }
164
+ if (!result) {
67
165
  return {
68
- assignmentResult: { networkId, assigned: true, success: true },
69
- mutationResult: { success: true, message: 'That intent is already in this network.' },
166
+ agentTimings: agentTimingsAccum,
167
+ evaluation: null,
168
+ shouldAssign: false,
169
+ finalScore: 0,
170
+ mutationResult: { success: false, error: "Evaluation returned no result." },
70
171
  };
71
172
  }
72
- if (outcome.kind === 'membership_required') {
173
+ const decision = buildNetworkAssignmentDecision({
174
+ resourceType: "intent",
175
+ mode: "automatic",
176
+ scope: "network",
177
+ indexPrompt,
178
+ memberPrompt,
179
+ rawScores: { indexScore: result.indexScore, memberScore: result.memberScore },
180
+ evaluator: "intent-networker",
181
+ source: "intent-network-graph",
182
+ reason: result.reasoning,
183
+ createdAt: new Date().toISOString(),
184
+ });
185
+ if (decision.assigned) {
186
+ const finalized = await finalizeAssignment(deps, state.userId, intentId, networkId, decision.finalScore, decision.metadata, `Intent assigned to network (score: ${decision.finalScore.toFixed(2)}).`);
73
187
  return {
74
- assignmentResult: { networkId, assigned: false, success: false },
75
- mutationResult: { success: false, error: 'Current network membership is required to assign this intent.' },
188
+ agentTimings: agentTimingsAccum,
189
+ evaluation: result,
190
+ shouldAssign: finalized.assignmentResult.assigned,
191
+ finalScore: decision.finalScore,
192
+ ...finalized,
76
193
  };
77
194
  }
78
195
  return {
79
- assignmentResult: { networkId, assigned: false, success: false },
80
- mutationResult: { success: false, error: 'Intent is no longer available for assignment.' },
196
+ agentTimings: agentTimingsAccum,
197
+ evaluation: result,
198
+ shouldAssign: false,
199
+ finalScore: decision.finalScore,
200
+ assignmentResult: { networkId, assigned: false, success: true },
201
+ mutationResult: { success: false, error: `Intent did not qualify for this network (score: ${decision.finalScore.toFixed(2)}).` },
81
202
  };
82
- };
83
- // --- NODE DEFINITIONS ---
84
- /**
85
- * Assign Node: Assign an intent to a network.
86
- *
87
- * Sub-paths selected by `skipEvaluation`:
88
- * - true → direct assignment (manual_override)
89
- * - false → evaluated assignment (automatic, with optional LLM fast-path)
90
- */
91
- const assignNode = async (state) => {
92
- return timed("IntentNetworkGraph.assign", async () => {
93
- const intentId = state.intentId;
94
- const networkId = state.networkId;
95
- logger.verbose("Assign intent to network", { userId: state.userId, intentId, networkId, skipEvaluation: state.skipEvaluation });
96
- const agentTimingsAccum = [];
97
- if (!intentId || !networkId) {
98
- return { agentTimings: agentTimingsAccum, mutationResult: { success: false, error: "Both intentId and networkId are required." } };
99
- }
100
- try {
101
- // Validate ownership and membership
102
- const intent = await this.database.getIntent(intentId);
103
- if (!intent) {
104
- return { agentTimings: agentTimingsAccum, mutationResult: { success: false, error: "Intent not found." } };
105
- }
106
- if (intent.userId !== state.userId) {
107
- return { agentTimings: agentTimingsAccum, mutationResult: { success: false, error: "You can only add your own intents to a network." } };
108
- }
109
- const [isMember, isOwner] = await Promise.all([
110
- this.database.isNetworkMember(networkId, state.userId),
111
- this.database.isIndexOwner(networkId, state.userId),
112
- ]);
113
- if (!isMember && !isOwner) {
114
- return { agentTimings: agentTimingsAccum, mutationResult: { success: false, error: "You are not a member of that network." } };
115
- }
116
- // Check if already assigned
117
- const alreadyAssigned = await this.database.isIntentAssignedToIndex(intentId, networkId);
118
- if (alreadyAssigned) {
119
- return { agentTimings: agentTimingsAccum, mutationResult: { success: true, message: "That intent is already in this network." } };
120
- }
121
- // Direct assignment (skip LLM evaluation)
122
- if (state.skipEvaluation) {
123
- const decision = buildNetworkAssignmentDecision({
124
- resourceType: "intent",
125
- mode: "manual_override",
126
- scope: "network",
127
- evaluator: "intent-network-graph",
128
- source: "manual-index-assignment",
129
- createdAt: new Date().toISOString(),
130
- });
131
- const finalized = await finalizeAssignment(state.userId, intentId, networkId, decision.finalScore, decision.metadata, 'Intent saved to the network.');
132
- return { agentTimings: agentTimingsAccum, ...finalized };
133
- }
134
- // Evaluated assignment path
135
- const intentForIndexing = await this.database.getIntentForIndexing(intentId);
136
- if (!intentForIndexing) {
137
- return { agentTimings: agentTimingsAccum, mutationResult: { success: false, error: "Intent not found for evaluation." } };
138
- }
139
- const indexContext = await this.database.getNetworkAssignmentContext(networkId, intentForIndexing.userId);
140
- if (!indexContext) {
141
- return {
142
- agentTimings: agentTimingsAccum,
143
- assignmentResult: { networkId, assigned: false, success: false },
144
- mutationResult: { success: false, error: "Network assignment context not found." },
145
- };
146
- }
147
- const indexPrompt = indexContext.indexPrompt ?? null;
148
- const memberPrompt = indexContext.memberPrompt ?? null;
149
- const hasNoPrompts = !indexPrompt?.trim() && !memberPrompt?.trim();
150
- // No-prompt fast path: assign without LLM
151
- if (hasNoPrompts) {
152
- const decision = buildNetworkAssignmentDecision({
153
- resourceType: "intent",
154
- mode: "automatic",
155
- scope: "network",
156
- indexPrompt,
157
- memberPrompt,
158
- evaluator: "intent-networker",
159
- source: "intent-network-graph",
160
- createdAt: new Date().toISOString(),
161
- });
162
- const finalized = await finalizeAssignment(state.userId, intentId, networkId, decision.finalScore, decision.metadata, 'Intent assigned to network (no prompts).');
163
- return { agentTimings: agentTimingsAccum, ...finalized };
164
- }
165
- // Render network context for the evaluator
166
- const network = await this.database.getNetwork(networkId);
167
- const renderedContext = network
168
- ? renderNetworkContext({
169
- title: network.title,
170
- prompt: network.prompt,
171
- })
172
- : null;
173
- const sourceName = intentForIndexing.sourceType
174
- ? `${intentForIndexing.sourceType}:${intentForIndexing.sourceId ?? ""}`
175
- : undefined;
176
- // Run IntentIndexer evaluation (injected from signals public facade via ports)
177
- const _traceEmitter = requestContext.getStore()?.traceEmitter;
178
- const _indexerStart = Date.now();
179
- _traceEmitter?.({ type: "agent_start", name: "intent-networker" });
180
- let result = null;
181
- try {
182
- result = await indexer.evaluate(intentForIndexing.payload, indexPrompt, memberPrompt, sourceName, renderedContext);
183
- }
184
- finally {
185
- const _indexerMs = Date.now() - _indexerStart;
186
- agentTimingsAccum.push({ name: 'intent.indexer', durationMs: _indexerMs });
187
- _traceEmitter?.({
188
- type: "agent_end",
189
- name: "intent-networker",
190
- durationMs: _indexerMs,
191
- summary: result
192
- ? `Scored: index=${result.indexScore.toFixed(2)}, member=${result.memberScore.toFixed(2)}`
193
- : "intent-networker failed",
194
- });
195
- }
196
- if (!result) {
197
- return {
198
- agentTimings: agentTimingsAccum,
199
- evaluation: null,
200
- shouldAssign: false,
201
- finalScore: 0,
202
- mutationResult: { success: false, error: "Evaluation returned no result." },
203
- };
204
- }
205
- const decision = buildNetworkAssignmentDecision({
206
- resourceType: "intent",
207
- mode: "automatic",
208
- scope: "network",
209
- indexPrompt,
210
- memberPrompt,
211
- rawScores: { indexScore: result.indexScore, memberScore: result.memberScore },
212
- evaluator: "intent-networker",
213
- source: "intent-network-graph",
214
- reason: result.reasoning,
215
- createdAt: new Date().toISOString(),
216
- });
217
- if (decision.assigned) {
218
- const finalized = await finalizeAssignment(state.userId, intentId, networkId, decision.finalScore, decision.metadata, `Intent assigned to network (score: ${decision.finalScore.toFixed(2)}).`);
219
- return {
220
- agentTimings: agentTimingsAccum,
221
- evaluation: result,
222
- shouldAssign: finalized.assignmentResult.assigned,
223
- finalScore: decision.finalScore,
224
- ...finalized,
225
- };
226
- }
227
- return {
228
- agentTimings: agentTimingsAccum,
229
- evaluation: result,
230
- shouldAssign: false,
231
- finalScore: decision.finalScore,
232
- assignmentResult: { networkId, assigned: false, success: true },
233
- mutationResult: { success: false, error: `Intent did not qualify for this network (score: ${decision.finalScore.toFixed(2)}).` },
234
- };
235
- }
236
- catch (err) {
237
- logger.error("Assign failed", { error: err });
238
- return { agentTimings: agentTimingsAccum, mutationResult: { success: false, error: "Failed to assign intent to network." } };
239
- }
240
- });
241
- };
242
- /**
243
- * Read Node: Query intent–network relationships.
244
- *
245
- * Three modes (selected by which inputs are provided):
246
- * - Both intentId + networkId: check if the specific link exists (owner only).
247
- * - intentId only: list all networks the intent is linked to (owner only).
248
- * - networkId only: list intents in the network (member only, up to 50).
249
- * Add `queryUserId` to filter to one member's intents.
250
- */
251
- const readNode = async (state) => {
252
- return timed("IntentNetworkGraph.read", async () => {
253
- const intentId = state.intentId;
254
- const networkId = state.networkId;
255
- logger.verbose("Read intent–network links", { userId: state.userId, intentId, networkId, queryUserId: state.queryUserId });
256
- try {
257
- // By both: check if specific intent–network link exists
258
- if (intentId && networkId) {
259
- const intent = await this.database.getIntent(intentId);
260
- if (!intent) {
261
- return { readResult: { links: [], count: 0, mode: "check_link" }, error: "Intent not found." };
262
- }
263
- if (intent.userId !== state.userId) {
264
- return { readResult: { links: [], count: 0, mode: "check_link" }, error: "You can only check links for your own intents." };
265
- }
266
- const isLinked = await this.database.isIntentAssignedToIndex(intentId, networkId);
267
- return {
268
- readResult: {
269
- links: isLinked ? [{ intentId, networkId }] : [],
270
- count: isLinked ? 1 : 0,
271
- mode: "check_link",
272
- note: isLinked ? "Intent is linked to this network." : "Intent is not linked to this network.",
273
- },
274
- };
275
- }
276
- // By intent only: list all networks for this intent
277
- if (intentId) {
278
- const intent = await this.database.getIntent(intentId);
279
- if (!intent) {
280
- return { readResult: { links: [], count: 0, mode: "networks_for_intent" }, error: "Intent not found." };
281
- }
282
- if (intent.userId !== state.userId) {
283
- return { readResult: { links: [], count: 0, mode: "networks_for_intent" }, error: "You can only list networks for your own intents." };
284
- }
285
- const networkIds = await this.database.getNetworkIdsForIntent(intentId);
286
- return {
287
- readResult: {
288
- links: networkIds.map((id) => ({ intentId, networkId: id })),
289
- count: networkIds.length,
290
- mode: "networks_for_intent",
291
- note: "To show network titles, use read_networks.",
292
- },
293
- };
294
- }
295
- // By network: list intents in the network
296
- if (!networkId) {
297
- return {
298
- readResult: { links: [], count: 0, mode: "unknown" },
299
- error: "Provide networkId or intentId.",
300
- };
301
- }
302
- const [isMember, isOwner] = await Promise.all([
303
- this.database.isNetworkMember(networkId, state.userId),
304
- this.database.isIndexOwner(networkId, state.userId),
305
- ]);
306
- if (!isMember && !isOwner) {
307
- return {
308
- readResult: { links: [], count: 0, mode: "intents_in_network" },
309
- error: "Network not found or you are not a member.",
310
- };
311
- }
312
- if (!state.queryUserId) {
313
- const intents = await this.database.getNetworkIntentsForMember(networkId, state.userId, { limit: 50, offset: 0 });
314
- return {
315
- readResult: {
316
- links: intents.map((i) => ({
317
- intentId: i.id,
318
- networkId,
319
- intentTitle: i.payload,
320
- userId: i.userId,
321
- userName: i.userName,
322
- createdAt: i.createdAt,
323
- relevancyScore: i.relevancyScore,
324
- })),
325
- count: intents.length,
326
- mode: "intents_in_network",
327
- note: "To show network title and full intent details, use read_networks and read_intents.",
328
- },
329
- };
330
- }
331
- // Specific user's intents in the network
332
- const intents = await this.database.getIntentsInIndexForMember(state.queryUserId, networkId);
333
- return {
334
- readResult: {
335
- links: intents.map((i) => ({
336
- intentId: i.id,
337
- networkId,
338
- intentTitle: i.payload,
339
- createdAt: i.createdAt,
340
- relevancyScore: i.relevancyScore,
341
- })),
342
- count: intents.length,
343
- mode: "intents_in_network",
344
- note: "To show network title and full intent details, use read_networks and read_intents.",
345
- },
346
- };
347
- }
348
- catch (err) {
349
- logger.error("Read intent–network failed", { error: err });
350
- return { error: "Failed to fetch intent–network links." };
203
+ }
204
+ catch (err) {
205
+ logger.error("Assign failed", { error: err });
206
+ return { agentTimings: agentTimingsAccum, mutationResult: { success: false, error: "Failed to assign intent to network." } };
207
+ }
208
+ });
209
+ }
210
+ /**
211
+ * Read Node: Query intent–network relationships.
212
+ *
213
+ * Three modes (selected by which inputs are provided):
214
+ * - Both intentId + networkId: check if the specific link exists (owner only).
215
+ * - intentId only: list all networks the intent is linked to (owner only).
216
+ * - networkId only: list intents in the network (member only, up to 50).
217
+ * Add `queryUserId` to filter to one member's intents.
218
+ */
219
+ export async function readNode(state, deps) {
220
+ return timed("IntentNetworkGraph.read", async () => {
221
+ const intentId = state.intentId;
222
+ const networkId = state.networkId;
223
+ logger.verbose("Read intent–network links", { userId: state.userId, intentId, networkId, queryUserId: state.queryUserId });
224
+ try {
225
+ // By both: check if specific intent–network link exists
226
+ if (intentId && networkId) {
227
+ const intent = await deps.database.getIntent(intentId);
228
+ if (!intent) {
229
+ return { readResult: { links: [], count: 0, mode: "check_link" }, error: "Intent not found." };
351
230
  }
352
- });
353
- };
354
- /**
355
- * Unassign Node: Remove an intent from a network.
356
- * Only the intent owner may unassign, and only if they are a member/owner of the network.
357
- */
358
- const unassignNode = async (state) => {
359
- return timed("IntentNetworkGraph.unassign", async () => {
360
- const intentId = state.intentId;
361
- const networkId = state.networkId;
362
- logger.verbose("Unassign intent from network", { userId: state.userId, intentId, networkId });
363
- if (!intentId || !networkId) {
364
- return { mutationResult: { success: false, error: "Both intentId and networkId are required." } };
231
+ if (intent.userId !== state.userId) {
232
+ return { readResult: { links: [], count: 0, mode: "check_link" }, error: "You can only check links for your own intents." };
365
233
  }
366
- try {
367
- const intent = await this.database.getIntent(intentId);
368
- if (!intent) {
369
- return { mutationResult: { success: false, error: "Intent not found." } };
370
- }
371
- if (intent.userId !== state.userId) {
372
- return { mutationResult: { success: false, error: "You can only remove your own intents from a network." } };
373
- }
374
- const [isMember, isOwner] = await Promise.all([
375
- this.database.isNetworkMember(networkId, state.userId),
376
- this.database.isIndexOwner(networkId, state.userId),
377
- ]);
378
- if (!isMember && !isOwner) {
379
- return { mutationResult: { success: false, error: "You are not a member of that network." } };
380
- }
381
- const assigned = await this.database.isIntentAssignedToIndex(intentId, networkId);
382
- if (!assigned) {
383
- return { mutationResult: { success: true, message: "That intent is not in this network." } };
384
- }
385
- await this.database.unassignIntentFromIndex(intentId, networkId);
386
- return { mutationResult: { success: true, message: "Intent removed from the network." } };
234
+ const isLinked = await deps.database.isIntentAssignedToIndex(intentId, networkId);
235
+ return {
236
+ readResult: {
237
+ links: isLinked ? [{ intentId, networkId }] : [],
238
+ count: isLinked ? 1 : 0,
239
+ mode: "check_link",
240
+ note: isLinked ? "Intent is linked to this network." : "Intent is not linked to this network.",
241
+ },
242
+ };
243
+ }
244
+ // By intent only: list all networks for this intent
245
+ if (intentId) {
246
+ const intent = await deps.database.getIntent(intentId);
247
+ if (!intent) {
248
+ return { readResult: { links: [], count: 0, mode: "networks_for_intent" }, error: "Intent not found." };
387
249
  }
388
- catch (err) {
389
- logger.error("Unassign failed", { error: err });
390
- return { mutationResult: { success: false, error: "Failed to remove intent from network." } };
250
+ if (intent.userId !== state.userId) {
251
+ return { readResult: { links: [], count: 0, mode: "networks_for_intent" }, error: "You can only list networks for your own intents." };
391
252
  }
392
- });
393
- };
394
- // --- CONDITIONAL ROUTING ---
395
- const routeByMode = (state) => {
396
- switch (state.operationMode) {
397
- case 'create': return 'assign';
398
- case 'read': return 'read';
399
- case 'delete': return 'unassign';
400
- default: return 'read';
253
+ const networkIds = await deps.database.getNetworkIdsForIntent(intentId);
254
+ return {
255
+ readResult: {
256
+ links: networkIds.map((id) => ({ intentId, networkId: id })),
257
+ count: networkIds.length,
258
+ mode: "networks_for_intent",
259
+ note: "To show network titles, use read_networks.",
260
+ },
261
+ };
401
262
  }
402
- };
403
- // --- GRAPH ASSEMBLY ---
404
- const workflow = new StateGraph(IntentNetworkGraphState)
405
- .addNode("assign", assignNode)
406
- .addNode("read", readNode)
407
- .addNode("unassign", unassignNode)
408
- .addConditionalEdges(START, routeByMode, {
409
- assign: "assign",
410
- read: "read",
411
- unassign: "unassign",
412
- })
413
- .addEdge("assign", END)
414
- .addEdge("read", END)
415
- .addEdge("unassign", END);
416
- return workflow.compile();
263
+ // By network: list intents in the network
264
+ if (!networkId) {
265
+ return {
266
+ readResult: { links: [], count: 0, mode: "unknown" },
267
+ error: "Provide networkId or intentId.",
268
+ };
269
+ }
270
+ const [isMember, isOwner] = await Promise.all([
271
+ deps.database.isNetworkMember(networkId, state.userId),
272
+ deps.database.isIndexOwner(networkId, state.userId),
273
+ ]);
274
+ if (!isMember && !isOwner) {
275
+ return {
276
+ readResult: { links: [], count: 0, mode: "intents_in_network" },
277
+ error: "Network not found or you are not a member.",
278
+ };
279
+ }
280
+ if (!state.queryUserId) {
281
+ const intents = await deps.database.getNetworkIntentsForMember(networkId, state.userId, { limit: 50, offset: 0 });
282
+ return {
283
+ readResult: {
284
+ links: intents.map((i) => ({
285
+ intentId: i.id,
286
+ networkId,
287
+ intentTitle: i.payload,
288
+ userId: i.userId,
289
+ userName: i.userName,
290
+ createdAt: i.createdAt,
291
+ relevancyScore: i.relevancyScore,
292
+ })),
293
+ count: intents.length,
294
+ mode: "intents_in_network",
295
+ note: "To show network title and full intent details, use read_networks and read_intents.",
296
+ },
297
+ };
298
+ }
299
+ // Specific user's intents in the network
300
+ const intents = await deps.database.getIntentsInIndexForMember(state.queryUserId, networkId);
301
+ return {
302
+ readResult: {
303
+ links: intents.map((i) => ({
304
+ intentId: i.id,
305
+ networkId,
306
+ intentTitle: i.payload,
307
+ createdAt: i.createdAt,
308
+ relevancyScore: i.relevancyScore,
309
+ })),
310
+ count: intents.length,
311
+ mode: "intents_in_network",
312
+ note: "To show network title and full intent details, use read_networks and read_intents.",
313
+ },
314
+ };
315
+ }
316
+ catch (err) {
317
+ logger.error("Read intent–network failed", { error: err });
318
+ return { error: "Failed to fetch intent–network links." };
319
+ }
320
+ });
321
+ }
322
+ /**
323
+ * Unassign Node: Remove an intent from a network.
324
+ * Only the intent owner may unassign, and only if they are a member/owner of the network.
325
+ */
326
+ export async function unassignNode(state, deps) {
327
+ return timed("IntentNetworkGraph.unassign", async () => {
328
+ const intentId = state.intentId;
329
+ const networkId = state.networkId;
330
+ logger.verbose("Unassign intent from network", { userId: state.userId, intentId, networkId });
331
+ if (!intentId || !networkId) {
332
+ return { mutationResult: { success: false, error: "Both intentId and networkId are required." } };
333
+ }
334
+ try {
335
+ const intent = await deps.database.getIntent(intentId);
336
+ if (!intent) {
337
+ return { mutationResult: { success: false, error: "Intent not found." } };
338
+ }
339
+ if (intent.userId !== state.userId) {
340
+ return { mutationResult: { success: false, error: "You can only remove your own intents from a network." } };
341
+ }
342
+ const [isMember, isOwner] = await Promise.all([
343
+ deps.database.isNetworkMember(networkId, state.userId),
344
+ deps.database.isIndexOwner(networkId, state.userId),
345
+ ]);
346
+ if (!isMember && !isOwner) {
347
+ return { mutationResult: { success: false, error: "You are not a member of that network." } };
348
+ }
349
+ const assigned = await deps.database.isIntentAssignedToIndex(intentId, networkId);
350
+ if (!assigned) {
351
+ return { mutationResult: { success: true, message: "That intent is not in this network." } };
352
+ }
353
+ await deps.database.unassignIntentFromIndex(intentId, networkId);
354
+ return { mutationResult: { success: true, message: "Intent removed from the network." } };
355
+ }
356
+ catch (err) {
357
+ logger.error("Unassign failed", { error: err });
358
+ return { mutationResult: { success: false, error: "Failed to remove intent from network." } };
359
+ }
360
+ });
361
+ }
362
+ export function routeByMode(state) {
363
+ switch (state.operationMode) {
364
+ case 'create': return 'assign';
365
+ case 'read': return 'read';
366
+ case 'delete': return 'unassign';
367
+ default: return 'read';
417
368
  }
418
369
  }