@indexnetwork/protocol 4.14.2-rc.356.1 → 4.16.0-rc.358.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 (61) hide show
  1. package/CHANGELOG.md +2 -0
  2. package/dist/index.d.ts +13 -3
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +6 -2
  5. package/dist/index.js.map +1 -1
  6. package/dist/opportunity/discriminator/discriminator.env.d.ts +15 -0
  7. package/dist/opportunity/discriminator/discriminator.env.d.ts.map +1 -1
  8. package/dist/opportunity/discriminator/discriminator.env.js +15 -0
  9. package/dist/opportunity/discriminator/discriminator.env.js.map +1 -1
  10. package/dist/opportunity/discriminator/discriminator.push.d.ts +16 -0
  11. package/dist/opportunity/discriminator/discriminator.push.d.ts.map +1 -0
  12. package/dist/opportunity/discriminator/discriminator.push.js +26 -0
  13. package/dist/opportunity/discriminator/discriminator.push.js.map +1 -0
  14. package/dist/opportunity/opportunity.graph.d.ts +13 -0
  15. package/dist/opportunity/opportunity.graph.d.ts.map +1 -1
  16. package/dist/opportunity/opportunity.graph.js +5 -1
  17. package/dist/opportunity/opportunity.graph.js.map +1 -1
  18. package/dist/opportunity/opportunity.state.d.ts +3 -0
  19. package/dist/opportunity/opportunity.state.d.ts.map +1 -1
  20. package/dist/opportunity/opportunity.state.js.map +1 -1
  21. package/dist/shared/agent/model.config.d.ts +5 -0
  22. package/dist/shared/agent/model.config.d.ts.map +1 -1
  23. package/dist/shared/agent/model.config.js +1 -0
  24. package/dist/shared/agent/model.config.js.map +1 -1
  25. package/dist/shared/hyde/hyde.documents.d.ts +10 -0
  26. package/dist/shared/hyde/hyde.documents.d.ts.map +1 -0
  27. package/dist/shared/hyde/hyde.documents.js +50 -0
  28. package/dist/shared/hyde/hyde.documents.js.map +1 -0
  29. package/dist/shared/hyde/hyde.env.d.ts +9 -0
  30. package/dist/shared/hyde/hyde.env.d.ts.map +1 -0
  31. package/dist/shared/hyde/hyde.env.js +10 -0
  32. package/dist/shared/hyde/hyde.env.js.map +1 -0
  33. package/dist/shared/hyde/hyde.frame.d.ts +151 -0
  34. package/dist/shared/hyde/hyde.frame.d.ts.map +1 -0
  35. package/dist/shared/hyde/hyde.frame.js +108 -0
  36. package/dist/shared/hyde/hyde.frame.js.map +1 -0
  37. package/dist/shared/hyde/hyde.generator.d.ts +23 -11
  38. package/dist/shared/hyde/hyde.generator.d.ts.map +1 -1
  39. package/dist/shared/hyde/hyde.generator.js +50 -21
  40. package/dist/shared/hyde/hyde.generator.js.map +1 -1
  41. package/dist/shared/hyde/hyde.graph.d.ts +58 -17
  42. package/dist/shared/hyde/hyde.graph.d.ts.map +1 -1
  43. package/dist/shared/hyde/hyde.graph.js +333 -89
  44. package/dist/shared/hyde/hyde.graph.js.map +1 -1
  45. package/dist/shared/hyde/hyde.state.d.ts +19 -2
  46. package/dist/shared/hyde/hyde.state.d.ts.map +1 -1
  47. package/dist/shared/hyde/hyde.state.js +23 -3
  48. package/dist/shared/hyde/hyde.state.js.map +1 -1
  49. package/dist/shared/hyde/hyde.validator.d.ts +79 -0
  50. package/dist/shared/hyde/hyde.validator.d.ts.map +1 -0
  51. package/dist/shared/hyde/hyde.validator.js +49 -0
  52. package/dist/shared/hyde/hyde.validator.js.map +1 -0
  53. package/dist/shared/hyde/lens.inferrer.d.ts +190 -12
  54. package/dist/shared/hyde/lens.inferrer.d.ts.map +1 -1
  55. package/dist/shared/hyde/lens.inferrer.js +93 -38
  56. package/dist/shared/hyde/lens.inferrer.js.map +1 -1
  57. package/dist/shared/schemas/question.schema.d.ts +283 -2
  58. package/dist/shared/schemas/question.schema.d.ts.map +1 -1
  59. package/dist/shared/schemas/question.schema.js +87 -0
  60. package/dist/shared/schemas/question.schema.js.map +1 -1
  61. package/package.json +2 -1
@@ -1,18 +1,27 @@
1
1
  /**
2
2
  * HyDE Graph: cache-aware hypothetical document generation with lens inference.
3
3
  *
4
- * Flow: infer_lenses → check_cache → (generate_missing if needed) → embed → cache_results.
5
- * Constructor injects Database, Embedder, Cache, LensInferrer, HydeGenerator.
4
+ * Legacy flow: infer_lenses → check_cache → generate_missing? → embed → cache_results.
5
+ * Frame-v1 flow adds one batch validate_generated node before embed.
6
6
  */
7
- import { StateGraph, START, END } from '@langchain/langgraph';
8
7
  import { createHash } from 'crypto';
8
+ import { END, START, StateGraph } from '@langchain/langgraph';
9
9
  import { getAbortSignalConfig } from '../agent/model-signal.js';
10
- import { HydeGraphState } from './hyde.state.js';
11
- import { HYDE_DEFAULT_CACHE_TTL } from './hyde.strategies.js';
12
10
  import { protocolLogger } from '../observability/protocol.logger.js';
13
11
  import { timed } from '../observability/performance.js';
14
12
  import { requestContext } from "../observability/request-context.js";
13
+ import { computeHydeSourceTextHash } from './hyde.documents.js';
14
+ import { getHydeGenerationMode, HYDE_FRAME_GENERATION_VERSION } from './hyde.env.js';
15
+ import { sanitizeHydeSourceFrame } from './hyde.frame.js';
16
+ import { HydeGraphState } from './hyde.state.js';
17
+ import { HYDE_DEFAULT_CACHE_TTL } from './hyde.strategies.js';
18
+ import { HydeValidator } from './hyde.validator.js';
15
19
  const logger = protocolLogger("HyDEGraphFactory");
20
+ let lastGenerationTimestamp = 0;
21
+ function nextGenerationMarker() {
22
+ lastGenerationTimestamp = Math.max(Date.now(), lastGenerationTimestamp + 1);
23
+ return new Date(lastGenerationTimestamp).toISOString();
24
+ }
16
25
  /** Hash a lens label (+ optional corpus) to a short key for cache/DB indexing. */
17
26
  function lensHash(label, corpus) {
18
27
  const input = corpus
@@ -20,149 +29,367 @@ function lensHash(label, corpus) {
20
29
  : label.toLowerCase().trim();
21
30
  return createHash('sha256').update(input).digest('hex').slice(0, 16);
22
31
  }
23
- /** Build cache key for a specific lens. */
24
- function cacheKey(sourceType, sourceId, sourceText, lens, corpus) {
25
- const entityKey = sourceId ?? `q:${createHash('sha256').update(sourceText).digest('hex').slice(0, 16)}`;
26
- return `hyde:${sourceType}:${entityKey}:${lensHash(lens, corpus)}`;
32
+ function entityCacheKey(sourceId, sourceText) {
33
+ return sourceId ?? `q:${createHash('sha256').update(sourceText).digest('hex').slice(0, 16)}`;
27
34
  }
28
- /**
29
- * Factory for the HyDE generation graph.
30
- * Injects Database, Embedder, Cache, LensInferrer, and HydeGenerator.
31
- */
35
+ function sortedFrame(frame) {
36
+ const sort = (items) => [...items].sort((left, right) => {
37
+ const leftJson = JSON.stringify(left);
38
+ const rightJson = JSON.stringify(right);
39
+ return leftJson < rightJson ? -1 : leftJson > rightJson ? 1 : 0;
40
+ });
41
+ return {
42
+ sourceRoles: sort(frame.sourceRoles),
43
+ counterpartRoles: sort(frame.counterpartRoles),
44
+ hardConstraints: sort(frame.hardConstraints),
45
+ namedEntities: sort(frame.namedEntities),
46
+ domainVocabulary: sort(frame.domainVocabulary),
47
+ };
48
+ }
49
+ /** Deterministic identity for the source content and sanitized frame. */
50
+ function computeHydeFrameFingerprint(sourceText, sourceFrame) {
51
+ return createHash('sha256')
52
+ .update(sourceText)
53
+ .update('\0')
54
+ .update(JSON.stringify(sortedFrame(sourceFrame)))
55
+ .digest('hex');
56
+ }
57
+ function requireFrameFingerprint(frameFingerprint) {
58
+ if (!frameFingerprint)
59
+ throw new Error('frame-v1 HyDE requires a frame fingerprint');
60
+ return frameFingerprint;
61
+ }
62
+ /** Preserve the exact legacy Redis key and isolate frame-v1 data by namespace. */
63
+ function cacheKey(mode, sourceType, sourceId, sourceText, lens, corpus, frameFingerprint) {
64
+ const entityKey = entityCacheKey(sourceId, sourceText);
65
+ if (mode === 'legacy')
66
+ return `hyde:${sourceType}:${entityKey}:${lensHash(lens, corpus)}`;
67
+ return `hyde:${HYDE_FRAME_GENERATION_VERSION}:${sourceType}:${entityKey}:${requireFrameFingerprint(frameFingerprint)}:${lensHash(lens, corpus)}`;
68
+ }
69
+ /** Preserve legacy identity and use a stable frame-v1 identity per lens/corpus. */
70
+ function dbStrategy(mode, label, corpus) {
71
+ const hash = lensHash(label, corpus);
72
+ return mode === 'legacy' ? hash : `${HYDE_FRAME_GENERATION_VERSION}:${hash}`;
73
+ }
74
+ function isValidGenerationMarker(value) {
75
+ return typeof value === 'string' && Number.isFinite(Date.parse(value));
76
+ }
77
+ function isFrameCacheDocument(doc, lensLabel, frameFingerprint, sourceTextHash) {
78
+ return doc.hydeGenerationVersion === HYDE_FRAME_GENERATION_VERSION
79
+ && doc.validationStatus === 'valid'
80
+ && doc.lens === lensLabel
81
+ && doc.frameFingerprint === frameFingerprint
82
+ && doc.sourceTextHash === sourceTextHash
83
+ && isValidGenerationMarker(doc.generatedAt);
84
+ }
85
+ function isFrameDbContext(context, lensLabel, frameFingerprint, sourceTextHash) {
86
+ return context?.hydeGenerationVersion === HYDE_FRAME_GENERATION_VERSION
87
+ && context.lensLabel === lensLabel
88
+ && context.validationStatus === 'valid'
89
+ && context.frameFingerprint === frameFingerprint
90
+ && context.sourceTextHash === sourceTextHash
91
+ && isValidGenerationMarker(context.generatedAt);
92
+ }
93
+ function emptyFrame() {
94
+ return {
95
+ sourceRoles: [],
96
+ counterpartRoles: [],
97
+ hardConstraints: [],
98
+ namedEntities: [],
99
+ domainVocabulary: [],
100
+ };
101
+ }
102
+ function opaqueDocumentKey(doc) {
103
+ return `d-${createHash('sha256')
104
+ .update(`${doc.lens}\0${doc.targetCorpus}\0${doc.hydeText}`)
105
+ .digest('hex')
106
+ .slice(0, 16)}`;
107
+ }
108
+ function isRuntimeVerdict(value) {
109
+ if (!value || typeof value !== 'object')
110
+ return false;
111
+ const verdict = value;
112
+ return typeof verdict.key === 'string'
113
+ && typeof verdict.valid === 'boolean'
114
+ && Array.isArray(verdict.unsupportedNamedEntities)
115
+ && verdict.unsupportedNamedEntities.every((item) => typeof item === 'string')
116
+ && Array.isArray(verdict.unsupportedHardConstraints)
117
+ && verdict.unsupportedHardConstraints.every((item) => typeof item === 'string')
118
+ && typeof verdict.reasoning === 'string';
119
+ }
120
+ /** Factory for the HyDE generation graph. Existing five-argument calls remain valid. */
32
121
  export class HydeGraphFactory {
33
- constructor(database, embedder, cache, inferrer, generator) {
122
+ constructor(database, embedder, cache, inferrer, generator, options = {}) {
34
123
  this.database = database;
35
124
  this.embedder = embedder;
36
125
  this.cache = cache;
37
126
  this.inferrer = inferrer;
38
127
  this.generator = generator;
128
+ this.options = options;
39
129
  }
40
130
  createGraph() {
41
131
  const self = this;
132
+ const mode = this.options.mode ?? getHydeGenerationMode();
133
+ const validator = mode === HYDE_FRAME_GENERATION_VERSION
134
+ ? (this.options.validator ?? new HydeValidator())
135
+ : undefined;
42
136
  /** Node 1: Infer lenses from source text + optional profile context. */
43
137
  const inferLensesNode = async (state) => {
44
138
  return timed("HydeGraph.inferLenses", async () => {
45
139
  const { sourceText, profileContext, maxLenses } = state;
46
- logger.verbose('Inferring lenses', { sourceTextLength: sourceText.length, hasProfileContext: !!profileContext });
47
140
  const agentTimingsAccum = [];
48
141
  try {
49
- const _traceEmitterLens = requestContext.getStore()?.traceEmitter;
142
+ const traceEmitter = requestContext.getStore()?.traceEmitter;
50
143
  const inferrerStart = Date.now();
51
- _traceEmitterLens?.({ type: "agent_start", name: "lens-inferrer" });
144
+ traceEmitter?.({ type: "agent_start", name: "lens-inferrer" });
52
145
  const result = await self.inferrer.infer({
53
146
  sourceText,
54
147
  profileContext,
55
148
  maxLenses,
149
+ ...(mode === HYDE_FRAME_GENERATION_VERSION ? { frameConstrained: true } : {}),
56
150
  });
57
- const _inferrerDuration = Date.now() - inferrerStart;
58
- agentTimingsAccum.push({ name: 'lens.inferrer', durationMs: _inferrerDuration });
59
- _traceEmitterLens?.({ type: "agent_end", name: "lens-inferrer", durationMs: _inferrerDuration, summary: result.lenses.length > 0 ? `Inferred ${result.lenses.length} lens(es)` : "lens-inferrer completed" });
60
- logger.verbose('Lenses inferred', {
61
- count: result.lenses.length,
62
- lenses: result.lenses.map(l => ({ label: l.label, corpus: l.corpus })),
63
- });
151
+ const durationMs = Date.now() - inferrerStart;
152
+ agentTimingsAccum.push({ name: 'lens.inferrer', durationMs });
153
+ traceEmitter?.({ type: "agent_end", name: "lens-inferrer", durationMs, summary: result.lenses.length > 0 ? `Inferred ${result.lenses.length} lens(es)` : "lens-inferrer completed" });
154
+ if (mode === HYDE_FRAME_GENERATION_VERSION) {
155
+ const sourceFrame = sanitizeHydeSourceFrame(sourceText, result.sourceFrame ?? emptyFrame());
156
+ return {
157
+ lenses: result.lenses,
158
+ sourceFrame,
159
+ frameFingerprint: computeHydeFrameFingerprint(sourceText, sourceFrame),
160
+ sourceTextHash: computeHydeSourceTextHash(sourceText),
161
+ generatedAt: nextGenerationMarker(),
162
+ agentTimings: agentTimingsAccum,
163
+ };
164
+ }
64
165
  return { lenses: result.lenses, agentTimings: agentTimingsAccum };
65
166
  }
66
167
  catch (error) {
67
168
  logger.error('Lens inference failed in graph node', { error });
169
+ if (mode === HYDE_FRAME_GENERATION_VERSION) {
170
+ const sourceFrame = emptyFrame();
171
+ return {
172
+ lenses: [],
173
+ sourceFrame,
174
+ frameFingerprint: computeHydeFrameFingerprint(sourceText, sourceFrame),
175
+ sourceTextHash: computeHydeSourceTextHash(sourceText),
176
+ generatedAt: nextGenerationMarker(),
177
+ agentTimings: agentTimingsAccum,
178
+ };
179
+ }
68
180
  return { lenses: [], agentTimings: agentTimingsAccum };
69
181
  }
70
182
  });
71
183
  };
72
- /** Node 2: Check cache/DB for existing HyDE docs matching inferred lenses. */
184
+ /** Node 2: Check the mode-isolated cache/DB for matching documents. */
73
185
  const checkCacheNode = async (state) => {
74
186
  return timed("HydeGraph.checkCache", async () => {
75
187
  const { sourceType, sourceId, sourceText, lenses, forceRegenerate } = state;
76
- if (forceRegenerate) {
77
- logger.verbose('Force regenerate - skipping cache');
188
+ if (forceRegenerate)
78
189
  return { hydeDocuments: {} };
79
- }
190
+ const frameFingerprint = mode === HYDE_FRAME_GENERATION_VERSION
191
+ ? requireFrameFingerprint(state.frameFingerprint)
192
+ : undefined;
193
+ const sourceTextHash = mode === HYDE_FRAME_GENERATION_VERSION
194
+ ? state.sourceTextHash ?? computeHydeSourceTextHash(sourceText)
195
+ : undefined;
80
196
  const cached = {};
81
197
  for (const lens of lenses) {
82
- const key = cacheKey(sourceType, sourceId ?? undefined, sourceText, lens.label, lens.corpus);
198
+ const key = cacheKey(mode, sourceType, sourceId ?? undefined, sourceText, lens.label, lens.corpus, frameFingerprint);
83
199
  const fromCache = await self.cache.get(key);
84
- if (fromCache?.hydeText && fromCache.hydeEmbedding?.length) {
85
- logger.verbose('Cache hit', { lens: lens.label });
86
- cached[lens.label] = fromCache;
200
+ const cacheAccepted = fromCache?.hydeText
201
+ && fromCache.hydeEmbedding?.length
202
+ && (mode === 'legacy' || isFrameCacheDocument(fromCache, lens.label, frameFingerprint, sourceTextHash));
203
+ if (cacheAccepted && fromCache) {
204
+ cached[lens.label] = {
205
+ ...fromCache,
206
+ ...(mode === HYDE_FRAME_GENERATION_VERSION ? { origin: 'cache' } : {}),
207
+ };
87
208
  continue;
88
209
  }
89
- // For entity sources, check DB for persisted docs
90
210
  if (sourceId) {
91
- const fromDb = await self.database.getHydeDocument(sourceType, sourceId, lensHash(lens.label, lens.corpus));
92
- if (fromDb) {
93
- logger.verbose('DB hit', { lens: lens.label });
211
+ const fromDb = await self.database.getHydeDocument(sourceType, sourceId, dbStrategy(mode, lens.label, lens.corpus));
212
+ const frameDbContext = mode === HYDE_FRAME_GENERATION_VERSION && fromDb
213
+ ? fromDb.context
214
+ : null;
215
+ if (fromDb && (mode === 'legacy' || isFrameDbContext(frameDbContext, lens.label, frameFingerprint, sourceTextHash))) {
94
216
  cached[lens.label] = {
95
217
  lens: lens.label,
96
218
  targetCorpus: fromDb.targetCorpus,
97
219
  hydeText: fromDb.hydeText,
98
220
  hydeEmbedding: fromDb.hydeEmbedding,
221
+ ...(mode === HYDE_FRAME_GENERATION_VERSION
222
+ ? {
223
+ origin: 'db',
224
+ validationStatus: 'valid',
225
+ hydeGenerationVersion: HYDE_FRAME_GENERATION_VERSION,
226
+ frameFingerprint,
227
+ sourceTextHash,
228
+ generatedAt: frameDbContext.generatedAt,
229
+ }
230
+ : {}),
99
231
  };
100
232
  }
101
233
  }
102
234
  }
103
- logger.verbose('Check cache done', {
104
- found: Object.keys(cached).length,
105
- requested: lenses.length,
106
- });
235
+ if (mode === HYDE_FRAME_GENERATION_VERSION) {
236
+ const newestTimestamp = Math.max(...Object.values(cached).map((doc) => Date.parse(doc.generatedAt ?? '')));
237
+ if (Number.isFinite(newestTimestamp)) {
238
+ return {
239
+ hydeDocuments: Object.fromEntries(Object.entries(cached).filter(([, doc]) => Date.parse(doc.generatedAt ?? '') === newestTimestamp)),
240
+ };
241
+ }
242
+ }
107
243
  return { hydeDocuments: cached };
108
244
  });
109
245
  };
110
- /** Conditional: decide whether to generate or skip to embed. */
111
- const shouldGenerate = (state) => {
112
- const { lenses, hydeDocuments } = state;
113
- const missing = lenses.filter((l) => !hydeDocuments[l.label]);
114
- if (missing.length > 0) {
115
- logger.verbose('Need to generate', { missing: missing.map(l => l.label) });
116
- return 'generate';
117
- }
118
- logger.verbose('All lenses cached, skipping generation');
119
- return 'skip';
120
- };
121
- /** Node 3: Generate HyDE documents for lenses not in cache. */
246
+ const shouldGenerate = (state) => state.lenses.some((lens) => !state.hydeDocuments[lens.label]) ? 'generate' : 'skip';
247
+ /** Node 3: Generate all missing documents and return a complete snapshot. */
122
248
  const generateMissingNode = async (state) => {
123
249
  return timed("HydeGraph.generateMissing", async () => {
124
- const { sourceText, lenses, hydeDocuments } = state;
125
- const missing = lenses.filter((l) => !hydeDocuments[l.label]);
126
- logger.verbose('Generating HyDE documents', {
127
- count: missing.length,
128
- lenses: missing.map(l => l.label),
129
- });
250
+ const { sourceText, sourceFrame, lenses, hydeDocuments } = state;
251
+ const missing = lenses.filter((lens) => !hydeDocuments[lens.label]);
130
252
  const agentTimingsAccum = [];
131
253
  const generated = {};
254
+ const sourceTextHash = mode === HYDE_FRAME_GENERATION_VERSION
255
+ ? state.sourceTextHash ?? computeHydeSourceTextHash(sourceText)
256
+ : undefined;
257
+ const generatedAt = mode === HYDE_FRAME_GENERATION_VERSION
258
+ ? state.generatedAt ?? nextGenerationMarker()
259
+ : undefined;
132
260
  await Promise.all(missing.map(async (lens) => {
133
- const _traceEmitterHyde = requestContext.getStore()?.traceEmitter;
261
+ const traceEmitter = requestContext.getStore()?.traceEmitter;
134
262
  const generatorStart = Date.now();
135
- _traceEmitterHyde?.({ type: "agent_start", name: "hyde-generator" });
263
+ traceEmitter?.({ type: "agent_start", name: "hyde-generator" });
136
264
  const out = await self.generator.generate({
137
265
  sourceText,
138
266
  lens: lens.label,
139
267
  corpus: lens.corpus,
268
+ ...(mode === HYDE_FRAME_GENERATION_VERSION && sourceFrame ? { sourceFrame } : {}),
140
269
  });
141
- const _hydeDuration = Date.now() - generatorStart;
142
- agentTimingsAccum.push({ name: 'hyde.generator', durationMs: _hydeDuration });
143
- _traceEmitterHyde?.({ type: "agent_end", name: "hyde-generator", durationMs: _hydeDuration, summary: `Generated: ${lens.label}` });
270
+ const durationMs = Date.now() - generatorStart;
271
+ agentTimingsAccum.push({ name: 'hyde.generator', durationMs });
272
+ traceEmitter?.({ type: "agent_end", name: "hyde-generator", durationMs, summary: `Generated: ${lens.label}` });
144
273
  generated[lens.label] = {
145
274
  lens: lens.label,
146
275
  targetCorpus: lens.corpus,
147
276
  hydeText: out.text,
148
277
  hydeEmbedding: [],
278
+ ...(mode === HYDE_FRAME_GENERATION_VERSION
279
+ ? {
280
+ origin: 'generated',
281
+ frameFingerprint: requireFrameFingerprint(state.frameFingerprint),
282
+ sourceTextHash,
283
+ generatedAt,
284
+ }
285
+ : {}),
149
286
  };
150
287
  }));
151
- return { hydeDocuments: { ...state.hydeDocuments, ...generated }, agentTimings: agentTimingsAccum };
288
+ const retained = mode === HYDE_FRAME_GENERATION_VERSION
289
+ ? Object.fromEntries(Object.entries(hydeDocuments).map(([label, doc]) => [
290
+ label,
291
+ {
292
+ ...doc,
293
+ frameFingerprint: requireFrameFingerprint(state.frameFingerprint),
294
+ sourceTextHash,
295
+ generatedAt,
296
+ },
297
+ ]))
298
+ : hydeDocuments;
299
+ return { hydeDocuments: { ...retained, ...generated }, agentTimings: agentTimingsAccum };
152
300
  });
153
301
  };
154
- /** Node 4: Embed all HyDE documents that don't have embeddings yet. */
302
+ /** Frame-v1 only: validate newly generated docs in one batch. */
303
+ const validateGeneratedNode = async (state) => {
304
+ return timed("HydeGraph.validateGenerated", async () => {
305
+ const generated = Object.values(state.hydeDocuments).filter((doc) => doc.origin === 'generated');
306
+ if (generated.length === 0 || !validator)
307
+ return { hydeDocuments: state.hydeDocuments };
308
+ const frame = sanitizeHydeSourceFrame(state.sourceText, state.sourceFrame ?? emptyFrame());
309
+ const documents = {};
310
+ const lensByDocumentKey = new Map();
311
+ for (const doc of generated) {
312
+ const key = opaqueDocumentKey(doc);
313
+ documents[key] = { corpus: doc.targetCorpus, text: doc.hydeText };
314
+ lensByDocumentKey.set(key, doc.lens);
315
+ }
316
+ const updated = { ...state.hydeDocuments };
317
+ const agentTimingsAccum = [];
318
+ const traceEmitter = requestContext.getStore()?.traceEmitter;
319
+ const validatorStart = Date.now();
320
+ let validCount = 0;
321
+ let rejectedCount = 0;
322
+ let failedOpenCount = 0;
323
+ traceEmitter?.({ type: 'agent_start', name: 'hyde-validator' });
324
+ try {
325
+ const output = await validator.validate({
326
+ sourceText: state.sourceText,
327
+ sourceFrame: frame,
328
+ documents,
329
+ });
330
+ const rawVerdicts = Array.isArray(output.verdicts)
331
+ ? output.verdicts
332
+ : [];
333
+ for (const key of Object.keys(documents)) {
334
+ const lensLabel = lensByDocumentKey.get(key);
335
+ if (!lensLabel)
336
+ continue;
337
+ const matching = rawVerdicts.filter((value) => !!value && typeof value === 'object' && value.key === key);
338
+ const doc = updated[lensLabel];
339
+ if (!doc)
340
+ continue;
341
+ if (matching.length !== 1 || !isRuntimeVerdict(matching[0])) {
342
+ failedOpenCount += 1;
343
+ updated[lensLabel] = { ...doc, validationStatus: 'failed_open', hydeGenerationVersion: HYDE_FRAME_GENERATION_VERSION };
344
+ continue;
345
+ }
346
+ const verdict = matching[0];
347
+ const hasUnsupportedGrounding = verdict.unsupportedNamedEntities.length > 0
348
+ || verdict.unsupportedHardConstraints.length > 0;
349
+ const contradictory = verdict.valid === hasUnsupportedGrounding;
350
+ if (contradictory) {
351
+ failedOpenCount += 1;
352
+ updated[lensLabel] = { ...doc, validationStatus: 'failed_open', hydeGenerationVersion: HYDE_FRAME_GENERATION_VERSION };
353
+ }
354
+ else if (!verdict.valid) {
355
+ rejectedCount += 1;
356
+ delete updated[lensLabel];
357
+ }
358
+ else {
359
+ validCount += 1;
360
+ updated[lensLabel] = { ...doc, validationStatus: 'valid', hydeGenerationVersion: HYDE_FRAME_GENERATION_VERSION };
361
+ }
362
+ }
363
+ }
364
+ catch (error) {
365
+ logger.error('HyDE validation failed open', { error });
366
+ validCount = 0;
367
+ rejectedCount = 0;
368
+ failedOpenCount = generated.length;
369
+ for (const doc of generated) {
370
+ updated[doc.lens] = { ...doc, validationStatus: 'failed_open', hydeGenerationVersion: HYDE_FRAME_GENERATION_VERSION };
371
+ }
372
+ }
373
+ finally {
374
+ const durationMs = Date.now() - validatorStart;
375
+ agentTimingsAccum.push({ name: 'hyde.validator', durationMs });
376
+ traceEmitter?.({
377
+ type: 'agent_end',
378
+ name: 'hyde-validator',
379
+ durationMs,
380
+ summary: `${validCount} valid, ${rejectedCount} rejected, ${failedOpenCount} failed open`,
381
+ });
382
+ }
383
+ return { hydeDocuments: updated, agentTimings: agentTimingsAccum };
384
+ });
385
+ };
386
+ /** Embed all accepted/cached documents that do not have embeddings. */
155
387
  const embedNode = async (state) => {
156
388
  return timed("HydeGraph.embed", async () => {
157
- const { hydeDocuments } = state;
158
- const lensLabels = Object.keys(hydeDocuments);
159
389
  const toEmbed = [];
160
390
  const updated = {};
161
391
  const hydeEmbeddings = {};
162
- for (const label of lensLabels) {
163
- const doc = hydeDocuments[label];
164
- if (!doc)
165
- continue;
392
+ for (const [label, doc] of Object.entries(state.hydeDocuments)) {
166
393
  if (doc.hydeEmbedding?.length) {
167
394
  updated[label] = doc;
168
395
  hydeEmbeddings[label] = doc.hydeEmbedding;
@@ -172,12 +399,8 @@ export class HydeGraphFactory {
172
399
  }
173
400
  }
174
401
  if (toEmbed.length > 0) {
175
- logger.verbose('Embedding documents', { count: toEmbed.length });
176
- const texts = toEmbed.map((t) => t.doc.hydeText);
177
- const embeddings = await self.embedder.generate(texts, undefined, getAbortSignalConfig());
178
- const embeddingArray = Array.isArray(embeddings[0])
179
- ? embeddings
180
- : [embeddings];
402
+ const embeddings = await self.embedder.generate(toEmbed.map((item) => item.doc.hydeText), undefined, getAbortSignalConfig());
403
+ const embeddingArray = Array.isArray(embeddings[0]) ? embeddings : [embeddings];
181
404
  for (let i = 0; i < toEmbed.length; i++) {
182
405
  const { label, doc } = toEmbed[i];
183
406
  const embedding = embeddingArray[i] ?? [];
@@ -188,31 +411,43 @@ export class HydeGraphFactory {
188
411
  return { hydeDocuments: updated, hydeEmbeddings };
189
412
  });
190
413
  };
191
- /** Node 5: Cache results in Redis; persist to DB for entity sources. */
414
+ /** Cache/persist only legacy docs or successfully validated frame-v1 docs. */
192
415
  const cacheResultsNode = async (state) => {
193
416
  return timed("HydeGraph.cacheResults", async () => {
194
417
  const { sourceType, sourceId, sourceText, hydeDocuments } = state;
195
- for (const label of Object.keys(hydeDocuments)) {
196
- const doc = hydeDocuments[label];
197
- if (!doc)
418
+ const frameFingerprint = mode === HYDE_FRAME_GENERATION_VERSION
419
+ ? requireFrameFingerprint(state.frameFingerprint)
420
+ : undefined;
421
+ const sourceTextHash = mode === HYDE_FRAME_GENERATION_VERSION
422
+ ? state.sourceTextHash ?? computeHydeSourceTextHash(sourceText)
423
+ : undefined;
424
+ for (const [label, doc] of Object.entries(hydeDocuments)) {
425
+ if (mode === HYDE_FRAME_GENERATION_VERSION
426
+ && !isFrameCacheDocument(doc, label, frameFingerprint, sourceTextHash))
198
427
  continue;
199
- const key = cacheKey(sourceType, sourceId ?? undefined, sourceText, label, doc.targetCorpus);
428
+ const key = cacheKey(mode, sourceType, sourceId ?? undefined, sourceText, label, doc.targetCorpus, frameFingerprint);
200
429
  await self.cache.set(key, doc, { ttl: HYDE_DEFAULT_CACHE_TTL });
201
- // Persist to DB for entity sources (intent/profile)
202
430
  if (sourceId) {
203
431
  await self.database.saveHydeDocument({
204
432
  sourceType,
205
433
  sourceId,
206
- strategy: lensHash(label, doc.targetCorpus),
434
+ strategy: dbStrategy(mode, label, doc.targetCorpus),
207
435
  targetCorpus: doc.targetCorpus,
208
436
  hydeText: doc.hydeText,
209
437
  hydeEmbedding: doc.hydeEmbedding,
438
+ ...(mode === HYDE_FRAME_GENERATION_VERSION ? {
439
+ context: {
440
+ hydeGenerationVersion: HYDE_FRAME_GENERATION_VERSION,
441
+ lensLabel: label,
442
+ validationStatus: 'valid',
443
+ frameFingerprint: doc.frameFingerprint,
444
+ sourceTextHash: doc.sourceTextHash,
445
+ generatedAt: doc.generatedAt,
446
+ },
447
+ } : {}),
210
448
  });
211
449
  }
212
450
  }
213
- logger.verbose('Cached results', {
214
- count: Object.keys(hydeDocuments).length,
215
- });
216
451
  return {};
217
452
  });
218
453
  };
@@ -227,8 +462,17 @@ export class HydeGraphFactory {
227
462
  .addConditionalEdges('check_cache', shouldGenerate, {
228
463
  generate: 'generate_missing',
229
464
  skip: 'embed',
230
- })
231
- .addEdge('generate_missing', 'embed')
465
+ });
466
+ if (mode === HYDE_FRAME_GENERATION_VERSION) {
467
+ workflow
468
+ .addNode('validate_generated', validateGeneratedNode)
469
+ .addEdge('generate_missing', 'validate_generated')
470
+ .addEdge('validate_generated', 'embed');
471
+ }
472
+ else {
473
+ workflow.addEdge('generate_missing', 'embed');
474
+ }
475
+ workflow
232
476
  .addEdge('embed', 'cache_results')
233
477
  .addEdge('cache_results', END);
234
478
  return workflow.compile();