@devflow-tools/retrieval-engine 0.18.0

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 (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +24 -0
  3. package/dist/__tests__/retrieval-coordinator.test.d.ts +2 -0
  4. package/dist/__tests__/retrieval-coordinator.test.d.ts.map +1 -0
  5. package/dist/__tests__/retrieval-coordinator.test.js +391 -0
  6. package/dist/__tests__/retrieval-coordinator.test.js.map +1 -0
  7. package/dist/__tests__/retrieval-session.test.d.ts +2 -0
  8. package/dist/__tests__/retrieval-session.test.d.ts.map +1 -0
  9. package/dist/__tests__/retrieval-session.test.js +52 -0
  10. package/dist/__tests__/retrieval-session.test.js.map +1 -0
  11. package/dist/__tests__/rrf.test.d.ts +2 -0
  12. package/dist/__tests__/rrf.test.d.ts.map +1 -0
  13. package/dist/__tests__/rrf.test.js +37 -0
  14. package/dist/__tests__/rrf.test.js.map +1 -0
  15. package/dist/__tests__/support.d.ts +13 -0
  16. package/dist/__tests__/support.d.ts.map +1 -0
  17. package/dist/__tests__/support.js +106 -0
  18. package/dist/__tests__/support.js.map +1 -0
  19. package/dist/__tests__/task-kernel-adapter.test.d.ts +2 -0
  20. package/dist/__tests__/task-kernel-adapter.test.d.ts.map +1 -0
  21. package/dist/__tests__/task-kernel-adapter.test.js +56 -0
  22. package/dist/__tests__/task-kernel-adapter.test.js.map +1 -0
  23. package/dist/index.d.ts +8 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +8 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/packing.d.ts +20 -0
  28. package/dist/packing.d.ts.map +1 -0
  29. package/dist/packing.js +48 -0
  30. package/dist/packing.js.map +1 -0
  31. package/dist/ports.d.ts +2 -0
  32. package/dist/ports.d.ts.map +1 -0
  33. package/dist/ports.js +2 -0
  34. package/dist/ports.js.map +1 -0
  35. package/dist/retrieval-coordinator.d.ts +33 -0
  36. package/dist/retrieval-coordinator.d.ts.map +1 -0
  37. package/dist/retrieval-coordinator.js +778 -0
  38. package/dist/retrieval-coordinator.js.map +1 -0
  39. package/dist/retrieval-session.d.ts +18 -0
  40. package/dist/retrieval-session.d.ts.map +1 -0
  41. package/dist/retrieval-session.js +46 -0
  42. package/dist/retrieval-session.js.map +1 -0
  43. package/dist/rrf.d.ts +8 -0
  44. package/dist/rrf.d.ts.map +1 -0
  45. package/dist/rrf.js +34 -0
  46. package/dist/rrf.js.map +1 -0
  47. package/dist/sufficiency.d.ts +8 -0
  48. package/dist/sufficiency.d.ts.map +1 -0
  49. package/dist/sufficiency.js +19 -0
  50. package/dist/sufficiency.js.map +1 -0
  51. package/dist/task-kernel-adapter.d.ts +12 -0
  52. package/dist/task-kernel-adapter.d.ts.map +1 -0
  53. package/dist/task-kernel-adapter.js +92 -0
  54. package/dist/task-kernel-adapter.js.map +1 -0
  55. package/package.json +38 -0
@@ -0,0 +1,778 @@
1
+ import { stableRuntimeHash, } from '@devflow-tools/sdk';
2
+ import { packEvidence } from './packing.js';
3
+ import { reciprocalRankFusion } from './rrf.js';
4
+ import { decideSufficiency } from './sufficiency.js';
5
+ import { RETRIEVAL_COORDINATOR_VERSION } from './retrieval-session.js';
6
+ const RERANKER_POOL_LIMIT = 20;
7
+ export class RetrievalCoordinator {
8
+ options;
9
+ version;
10
+ constructor(options) {
11
+ this.options = options;
12
+ this.version = options.coordinatorVersion ?? RETRIEVAL_COORDINATOR_VERSION;
13
+ }
14
+ async retrieve(input) {
15
+ const now = input.now ?? Date.now();
16
+ const session = this.options.sessions.open({
17
+ context: input.context,
18
+ taskRevision: input.taskRevision,
19
+ tokenBudget: input.tokenBudget ?? executableChannelCount(input.context) * 2_000 + 2_000,
20
+ coordinatorVersion: this.version,
21
+ now,
22
+ });
23
+ const priorCycles = this.options.sessions.cycles(session.id);
24
+ await this.options.taskRuntime.sessionOpened({
25
+ identity: input.context.identity,
26
+ taskRevision: input.taskRevision,
27
+ sessionId: session.id,
28
+ factsHash: input.context.facts.factsHash,
29
+ planHash: session.planHash,
30
+ activationSnapshotHash: session.activationSnapshotHash,
31
+ coordinatorVersion: this.version,
32
+ });
33
+ for (const priorCycle of priorCycles)
34
+ await this.options.taskRuntime.cycleCompleted({
35
+ identity: input.context.identity,
36
+ taskRevision: input.taskRevision,
37
+ cycle: priorCycle,
38
+ terminal: session.state !== 'open' && priorCycle.cycle === session.cycle,
39
+ });
40
+ if (priorCycles.length === 0) {
41
+ this.observe('retrieval.session.opened', input.context, now, {
42
+ sessionId: session.id,
43
+ taskRevision: input.taskRevision,
44
+ factsHash: input.context.facts.factsHash,
45
+ planHash: session.planHash,
46
+ activationSnapshotHash: session.activationSnapshotHash,
47
+ coordinatorVersion: this.version,
48
+ tokenBudget: session.initialTokenBudget,
49
+ });
50
+ }
51
+ if (session.state !== 'open') {
52
+ const latest = priorCycles.at(-1);
53
+ if (!latest)
54
+ throw new Error('RETRIEVAL_TERMINAL_RECEIPT_MISSING');
55
+ return {
56
+ status: latest.sufficiency,
57
+ executionStatus: latest.executionStatus,
58
+ receipt: latest.compositeReceipt,
59
+ cycles: priorCycles,
60
+ };
61
+ }
62
+ const signatures = requestSignatures(input.context, input.taskRevision, priorCycles.at(-1)?.gaps ?? []);
63
+ const completedSignatures = new Set(priorCycles.flatMap(cycle => cycle.requestSignatures.map(signature => signature.signatureHash)));
64
+ const activeSignatures = signatures.filter(signature => !completedSignatures.has(signature.signatureHash));
65
+ const settled = await Promise.allSettled(activeSignatures.map(async (signature) => {
66
+ const producer = signature.channel === 'code' ? this.options.code
67
+ : signature.channel === 'memory' ? this.options.memory : this.options.knowledge;
68
+ const result = await producer.search(input.context, signature);
69
+ return { signature, result: validateProducerResult(signature, input.context, result) };
70
+ }));
71
+ const outputs = settled.map((entry, index) => entry.status === 'fulfilled'
72
+ ? { ...entry.value, failureStatus: undefined }
73
+ : {
74
+ signature: activeSignatures[index],
75
+ result: unavailable(activeSignatures[index].channel, input.context, providerFailureReason(entry.reason), now, activeSignatures[index]),
76
+ failureStatus: 'failed',
77
+ });
78
+ const currentAvailability = aggregateChannelAvailability(outputs);
79
+ const legs = outputs.map(output => legOutcome(output.signature, output.result, output.failureStatus));
80
+ const candidateReceipts = outputs.flatMap(output => output.result.candidates.map(candidate => evidenceStageReceipt(candidate, 'candidate', [output.result.receipt.receiptId], now)));
81
+ const rejected = [];
82
+ const eligible = outputs.flatMap(output => hardFilter(output.result.candidates.map(candidate => scopeCandidateLegs(output.signature, candidate)), output.signature.channel, input.context, rejected, now));
83
+ const eligibleRefs = new Set(eligible.map(candidate => candidate.stableRef));
84
+ const eligibilityReceipts = candidateReceipts.map(receipt => eligibleRefs.has(receipt.stableRef)
85
+ ? evidenceStageReceiptFrom(receipt, 'eligible', now)
86
+ : evidenceStageReceiptFrom(receipt, 'rejected', now, rejected.find(item => item.ref === receipt.stableRef)?.reason ?? 'ineligible'));
87
+ let fused = reciprocalRankFusion(eligible);
88
+ const fusedCount = fused.length;
89
+ let rerankerFailed = false;
90
+ let rerankerReceipt;
91
+ if (this.options.reranker && fused.length > 0) {
92
+ const pool = fused.slice(0, RERANKER_POOL_LIMIT);
93
+ const requestHash = stableRuntimeHash({
94
+ contextHash: input.context.contextHash,
95
+ candidates: pool.map(item => item.candidate.stableRef),
96
+ profileId: this.options.reranker.profileId,
97
+ profileVersion: this.options.reranker.profileVersion,
98
+ });
99
+ const startedAt = performance.now();
100
+ try {
101
+ for (const item of fused.slice(RERANKER_POOL_LIMIT)) {
102
+ rejected.push({ ref: item.candidate.stableRef, reason: 'reranker_pool_limit' });
103
+ }
104
+ fused = pool;
105
+ const reranked = await this.options.reranker.rerank({
106
+ context: input.context,
107
+ candidates: pool.map(item => item.candidate),
108
+ });
109
+ const poolRefs = new Set(pool.map(item => item.candidate.stableRef));
110
+ const scores = new Map();
111
+ for (const item of reranked) {
112
+ if (!poolRefs.has(item.stableRef) || scores.has(item.stableRef) || !Number.isFinite(item.score)) {
113
+ throw new Error('RERANKER_RESULT_INVALID');
114
+ }
115
+ scores.set(item.stableRef, item.score);
116
+ }
117
+ fused = [...pool].sort((left, right) => Number(scores.has(right.candidate.stableRef)) - Number(scores.has(left.candidate.stableRef))
118
+ || (scores.get(right.candidate.stableRef) ?? 0) - (scores.get(left.candidate.stableRef) ?? 0)
119
+ || right.rrfScore - left.rrfScore
120
+ || left.candidate.stableRef.localeCompare(right.candidate.stableRef));
121
+ rerankerReceipt = providerReceipt({
122
+ executionKey: `${session.id}:${session.cycle + 1}:reranker`, requestHash,
123
+ providerId: this.options.reranker.profileId,
124
+ providerVersion: this.options.reranker.profileVersion,
125
+ status: 'ready', proposalCount: pool.length, acceptedCount: scores.size,
126
+ rejectedCount: pool.length - scores.size, reasonCodes: [],
127
+ latencyMs: performance.now() - startedAt, createdAt: now,
128
+ });
129
+ this.observe('provider.execution.completed', input.context, now, providerLogData(rerankerReceipt));
130
+ }
131
+ catch (error) {
132
+ rerankerFailed = true;
133
+ fused = [];
134
+ const invalid = error instanceof Error && error.message === 'RERANKER_RESULT_INVALID';
135
+ rerankerReceipt = providerReceipt({
136
+ executionKey: `${session.id}:${session.cycle + 1}:reranker`, requestHash,
137
+ providerId: this.options.reranker.profileId,
138
+ providerVersion: this.options.reranker.profileVersion,
139
+ status: invalid ? 'invalid' : 'unavailable', proposalCount: pool.length,
140
+ acceptedCount: 0, rejectedCount: pool.length,
141
+ reasonCodes: [invalid ? 'reranker_result_invalid' : 'reranker_exception'],
142
+ latencyMs: performance.now() - startedAt, createdAt: now,
143
+ });
144
+ this.observe('provider.execution.rejected', input.context, now, providerLogData(rerankerReceipt));
145
+ }
146
+ }
147
+ const previousReceipt = priorCycles.at(-1)?.compositeReceipt;
148
+ const activeChannels = new Set(activeSignatures.map(signature => signature.channel));
149
+ const gaps = dedupeGaps([
150
+ ...(previousReceipt?.gaps ?? []).filter(gap => gap.resolver === 'none' || !activeChannels.has(gap.resolver)),
151
+ ...outputs.flatMap(output => currentAvailability[output.signature.channel] !== 'unavailable'
152
+ && output.result.availability === 'unavailable' ? [] : output.result.gaps),
153
+ ...(rerankerFailed ? [{
154
+ kind: 'unavailable_channel',
155
+ reason: 'reranker_unavailable',
156
+ evidenceRefs: [],
157
+ resolver: 'none',
158
+ priority: 1,
159
+ }] : []),
160
+ ]);
161
+ this.observe('retrieval.fusion.completed', input.context, now, {
162
+ sessionId: session.id,
163
+ cycle: session.cycle + 1,
164
+ candidateCount: outputs.reduce((sum, output) => sum + output.result.candidates.length, 0),
165
+ eligibleCount: eligible.length,
166
+ fusedCount,
167
+ rejectedCount: rejected.length,
168
+ contributingLegCount: new Set(eligible.flatMap(candidate => candidate.contributingLegs.map(leg => leg.leg))).size,
169
+ coordinatorVersion: this.version,
170
+ rerankerProfile: this.options.reranker
171
+ ? `${this.options.reranker.profileId}@${this.options.reranker.profileVersion}` : 'none',
172
+ rerankerStatus: rerankerReceipt?.status ?? 'not_configured',
173
+ });
174
+ const packed = packEvidence({
175
+ context: input.context,
176
+ fused,
177
+ rejected,
178
+ gaps,
179
+ tokenBudget: session.remainingTokenBudget,
180
+ coordinatorVersion: this.version,
181
+ parentReceiptIds: [
182
+ ...outputs.map(output => output.result.receipt.receiptId),
183
+ ...(rerankerReceipt ? [rerankerReceipt.receiptId] : []),
184
+ ],
185
+ createdAt: now,
186
+ });
187
+ const eligibleReceiptIds = evidenceReceiptIdsByRef(eligibilityReceipts.filter(receipt => receipt.stage === 'eligible'));
188
+ const rankedReceipts = fused.map((item, index) => evidenceStageReceipt(item.candidate, 'ranked', eligibleReceiptIds.get(item.candidate.stableRef) ?? [], now, undefined, index + 1));
189
+ const rankedReceiptIds = evidenceReceiptIdsByRef(rankedReceipts);
190
+ const candidateReceiptIds = evidenceReceiptIdsByRef(candidateReceipts);
191
+ const terminalReceipts = [
192
+ ...packed.selected.map((candidate, index) => evidenceStageReceipt(candidate, 'selected', rankedReceiptIds.get(candidate.stableRef)
193
+ ?? eligibleReceiptIds.get(candidate.stableRef)
194
+ ?? candidateReceiptIds.get(candidate.stableRef)
195
+ ?? [], now, undefined, index + 1)),
196
+ ...rejected.flatMap(item => {
197
+ const candidate = outputs.flatMap(output => output.result.candidates)
198
+ .find(value => value.stableRef === item.ref);
199
+ return candidate ? [evidenceStageReceipt(candidate, 'rejected', rankedReceiptIds.get(item.ref)
200
+ ?? eligibleReceiptIds.get(item.ref)
201
+ ?? candidateReceiptIds.get(item.ref)
202
+ ?? [], now, item.reason)] : [];
203
+ }),
204
+ ];
205
+ const cycleEvidenceReceipts = dedupeEvidenceStageReceipts([
206
+ ...candidateReceipts,
207
+ ...eligibilityReceipts,
208
+ ...rankedReceipts,
209
+ ...terminalReceipts,
210
+ ]);
211
+ const previousRefs = new Set(priorCycles.flatMap(cycle => cycle.selectedRefs));
212
+ const stableRefGain = packed.receipt.selectedRefs.filter(ref => !previousRefs.has(ref)).length;
213
+ const noGainCount = stableRefGain > 0 ? 0 : Math.min(2, (priorCycles.at(-1)?.noGainCount ?? 0) + 1);
214
+ const availability = {
215
+ ...(previousReceipt?.channelAvailability ?? {}),
216
+ ...currentAvailability,
217
+ };
218
+ const cumulativeSelectedRefs = dedupeStrings([
219
+ ...(previousReceipt?.selectedRefs ?? []),
220
+ ...packed.receipt.selectedRefs,
221
+ ]);
222
+ const selectedCounts = Object.fromEntries(['code', 'memory', 'knowledge'].map(channel => [
223
+ channel, channel === 'code' && requiresStrongCodeEvidence(input.context)
224
+ ? mergeSelectedEvidence(previousReceipt?.selectedEvidence, packed.receipt.selectedEvidence)
225
+ .filter(candidate => candidate.channel === 'code' && candidate.strength === 'strong').length
226
+ : new Set([
227
+ ...(previousReceipt?.selectedRefsByChannel?.[channel] ?? []),
228
+ ...(packed.receipt.selectedRefsByChannel?.[channel] ?? []),
229
+ ]).size,
230
+ ]));
231
+ let sufficiency = decideSufficiency({ context: input.context, availability, selectedCounts, gaps });
232
+ if (activeSignatures.length === 0 || noGainCount >= 2) {
233
+ sufficiency = gaps.some(gap => gap.kind === 'unavailable_channel') ? 'gap_unavailable' : 'abstain';
234
+ }
235
+ const cumulativeRejectedRefs = dedupeRejected([
236
+ ...(previousReceipt?.rejectedRefs ?? []),
237
+ ...rejected,
238
+ ]);
239
+ if (rejected.length > 0) {
240
+ this.observe('retrieval.candidate.rejected', input.context, now, {
241
+ sessionId: session.id,
242
+ cycle: session.cycle + 1,
243
+ rejectedCount: rejected.length,
244
+ reasonCounts: rejectionReasonCounts(rejected),
245
+ coordinatorVersion: this.version,
246
+ });
247
+ }
248
+ const executionStatus = retrievalExecutionStatus({
249
+ legs,
250
+ rawCandidateCount: outputs.reduce((sum, output) => sum + producerCandidateCount(output.result), 0),
251
+ eligibleCount: eligible.length,
252
+ rerankerFailed,
253
+ });
254
+ const compositeBody = {
255
+ ...packed.receipt,
256
+ parentReceiptIds: dedupeStrings([
257
+ ...(previousReceipt ? [previousReceipt.receiptId] : []),
258
+ ...legs.map(leg => leg.legId),
259
+ ...terminalReceipts.map(receipt => receipt.receiptId),
260
+ ...(rerankerReceipt ? [rerankerReceipt.receiptId] : []),
261
+ ]).sort(),
262
+ selectedRefs: cumulativeSelectedRefs,
263
+ selectedEvidence: mergeSelectedEvidence(previousReceipt?.selectedEvidence, packed.receipt.selectedEvidence),
264
+ rejectedRefs: cumulativeRejectedRefs,
265
+ contributingLegs: mergeContributingLegs(previousReceipt?.contributingLegs, packed.receipt.contributingLegs),
266
+ channelAvailability: availability,
267
+ selectedRefsByChannel: mergeSelectedRefsByChannel(previousReceipt?.selectedRefsByChannel, packed.receipt.selectedRefsByChannel),
268
+ legs: mergeLegOutcomes(previousReceipt?.legs, legs),
269
+ evidenceReceipts: dedupeEvidenceStageReceipts([
270
+ ...(previousReceipt?.evidenceReceipts ?? []),
271
+ ...cycleEvidenceReceipts,
272
+ ]),
273
+ ...(this.options.reranker ? {
274
+ reranker: {
275
+ profileId: this.options.reranker.profileId,
276
+ profileVersion: this.options.reranker.profileVersion,
277
+ ...(rerankerReceipt ?? previousReceipt?.reranker?.providerReceipt
278
+ ? { providerReceipt: (rerankerReceipt ?? previousReceipt?.reranker?.providerReceipt) }
279
+ : {}),
280
+ },
281
+ } : {}),
282
+ };
283
+ const { receiptHash: _packedReceiptHash, ...unhashedComposite } = compositeBody;
284
+ const compositeReceipt = {
285
+ ...unhashedComposite,
286
+ receiptHash: stableRuntimeHash(unhashedComposite),
287
+ };
288
+ const cycleBody = {
289
+ schemaVersion: 'retrieval-cycle-receipt.v1',
290
+ sessionId: session.id,
291
+ cycle: session.cycle + 1,
292
+ requestSignatures: activeSignatures,
293
+ legs,
294
+ selectedRefs: packed.receipt.selectedRefs,
295
+ rejectedRefs: rejected,
296
+ gaps,
297
+ tokenCost: packed.tokenCost,
298
+ stableRefGain,
299
+ noGainCount,
300
+ sufficiency,
301
+ executionStatus,
302
+ compositeReceipt,
303
+ evidenceHash: stableRuntimeHash({ selected: cumulativeSelectedRefs, gaps }),
304
+ createdAt: now,
305
+ };
306
+ const cycle = {
307
+ ...cycleBody,
308
+ receiptHash: stableRuntimeHash(cycleBody),
309
+ };
310
+ const persistedSession = this.options.sessions.append(session, cycle, session.remainingTokenBudget - packed.tokenCost);
311
+ await this.options.taskRuntime.cycleCompleted({
312
+ identity: input.context.identity,
313
+ taskRevision: input.taskRevision,
314
+ cycle,
315
+ terminal: persistedSession.state !== 'open',
316
+ });
317
+ this.observe('retrieval.cycle.completed', input.context, now, {
318
+ sessionId: session.id,
319
+ cycle: cycle.cycle,
320
+ receiptId: cycle.compositeReceipt.receiptId,
321
+ receiptHash: cycle.compositeReceipt.receiptHash,
322
+ selectedCount: cycle.selectedRefs.length,
323
+ rejectedCount: cycle.rejectedRefs.length,
324
+ gapCount: cycle.gaps.length,
325
+ stableRefGain: cycle.stableRefGain,
326
+ noGainCount: cycle.noGainCount,
327
+ tokenCost: cycle.tokenCost,
328
+ sufficiency: cycle.sufficiency,
329
+ coordinatorVersion: this.version,
330
+ });
331
+ this.observe('retrieval.selection.completed', input.context, now, {
332
+ sessionId: session.id,
333
+ cycle: cycle.cycle,
334
+ receiptId: cycle.compositeReceipt.receiptId,
335
+ receiptHash: cycle.compositeReceipt.receiptHash,
336
+ selectedCount: cycle.compositeReceipt.selectedRefs.length,
337
+ rejectedCount: cycle.compositeReceipt.rejectedRefs.length,
338
+ gapCount: cycle.compositeReceipt.gaps.length,
339
+ sufficiency: cycle.sufficiency,
340
+ coordinatorVersion: this.version,
341
+ });
342
+ if (persistedSession.state !== 'open') {
343
+ this.observe('retrieval.cycle.stopped', input.context, now, {
344
+ sessionId: session.id,
345
+ cycle: cycle.cycle,
346
+ reason: stopReason(cycle, session.remainingTokenBudget - packed.tokenCost),
347
+ sufficiency: cycle.sufficiency,
348
+ receiptHash: cycle.compositeReceipt.receiptHash,
349
+ });
350
+ }
351
+ return {
352
+ status: sufficiency,
353
+ executionStatus,
354
+ receipt: cycle.compositeReceipt,
355
+ cycles: [...priorCycles, cycle],
356
+ };
357
+ }
358
+ observe(name, context, timestamp, data) {
359
+ try {
360
+ this.options.observer?.record({ name, identity: context.identity, data, timestamp });
361
+ }
362
+ catch { }
363
+ }
364
+ }
365
+ function requestSignatures(context, taskRevision, openGaps) {
366
+ return ['code', 'memory', 'knowledge'].flatMap(channel => {
367
+ const channelPlan = context.plan[channel];
368
+ const channelGaps = openGaps.filter(gap => gap.resolver === channel)
369
+ .map(gap => ({
370
+ kind: gap.kind,
371
+ reasonCode: gap.reason,
372
+ ...(gap.target ? { target: gap.target } : {}),
373
+ evidenceRefs: [...gap.evidenceRefs].sort(),
374
+ priority: gap.priority,
375
+ }));
376
+ if (channelPlan.requirement === 'prohibited')
377
+ return [];
378
+ const typedQueryBase = channelGaps.length > 0
379
+ ? { planQuery: channelPlan.typedQuery, openGaps: channelGaps }
380
+ : { planQuery: channelPlan.typedQuery };
381
+ const embeddingPurpose = channel === 'code' ? 'code_retrieval'
382
+ : channel === 'memory' ? 'memory_retrieval' : 'knowledge_retrieval';
383
+ const scopeKinds = channel === 'memory'
384
+ ? new Set(context.plan.memory.requirement === 'prohibited' ? [] : context.plan.memory.typedQuery.scopes)
385
+ : new Set(['project']);
386
+ const scopes = [context.scopeBindings.project, context.scopeBindings.session, context.scopeBindings.skill]
387
+ .filter((scope) => Boolean(scope) && scopeKinds.has(scope.kind));
388
+ const minimumAuthority = channel === 'knowledge' && context.plan.knowledge.requirement !== 'prohibited'
389
+ ? context.plan.knowledge.typedQuery.minimumAuthority : undefined;
390
+ const filters = {
391
+ scopes,
392
+ embeddingPurpose,
393
+ ...(context.embeddingProfiles[embeddingPurpose]
394
+ ? { embeddingProfileHash: context.embeddingProfiles[embeddingPurpose] }
395
+ : {}),
396
+ ...(minimumAuthority !== undefined ? { minimumAuthority } : {}),
397
+ };
398
+ const variants = validateQueryVariants(channel, context.facts.sourceHash, [channelPlan.original, ...channelPlan.additions]);
399
+ return variants.map(queryVariant => {
400
+ const typedQuery = { ...typedQueryBase, queryVariant };
401
+ const body = {
402
+ schemaVersion: 'retrieval-request-signature.v1',
403
+ parentReceiptIds: [],
404
+ channel,
405
+ identity: context.identity,
406
+ taskRevision,
407
+ factsHash: context.facts.factsHash,
408
+ planHash: context.plan.planHash,
409
+ activationSnapshotHash: context.activationSnapshotHash,
410
+ queryVariant,
411
+ typedQuery,
412
+ filters,
413
+ };
414
+ const signatureHash = stableRuntimeHash(body);
415
+ return { ...body, receiptId: `retrieval-request:${signatureHash}`, signatureHash };
416
+ });
417
+ });
418
+ }
419
+ function validateQueryVariants(channel, sourceHash, variants) {
420
+ if (!Array.isArray(variants) || variants.length === 0
421
+ || variants.filter(variant => variant.kind === 'original').length !== 1) {
422
+ throw new Error(`RETRIEVAL_${channel.toUpperCase()}_QUERY_VARIANTS_INVALID`);
423
+ }
424
+ const seen = new Set();
425
+ return variants.map(variant => {
426
+ const query = variant.query.normalize('NFKC').trim().slice(0, 2_000);
427
+ if (!query || !variant.producerId || !variant.producerVersion
428
+ || variant.sourceHash !== sourceHash
429
+ || variant.queryHash !== stableRuntimeHash({ query, sourceHash })
430
+ || seen.has(`${variant.kind}:${variant.queryHash}`)
431
+ || new Set(variant.sourceClaimIds).size !== variant.sourceClaimIds.length) {
432
+ throw new Error(`RETRIEVAL_${channel.toUpperCase()}_QUERY_VARIANT_INVALID`);
433
+ }
434
+ seen.add(`${variant.kind}:${variant.queryHash}`);
435
+ return { ...variant, query, sourceClaimIds: [...variant.sourceClaimIds].sort() };
436
+ });
437
+ }
438
+ function aggregateChannelAvailability(outputs) {
439
+ const result = {};
440
+ for (const channel of ['code', 'memory', 'knowledge']) {
441
+ const channelOutputs = outputs.filter(output => output.signature.channel === channel);
442
+ if (channelOutputs.length === 0)
443
+ continue;
444
+ const requiredOutputs = channelOutputs.filter(output => output.signature.queryVariant.kind === 'original');
445
+ result[channel] = requiredOutputs.some(output => output.result.availability === 'unavailable')
446
+ ? 'unavailable'
447
+ : channelOutputs.some(output => output.result.availability === 'available')
448
+ ? 'available'
449
+ : 'empty_valid';
450
+ }
451
+ return result;
452
+ }
453
+ function legOutcome(signature, result, failureStatus) {
454
+ const legId = `retrieval-leg:${stableRuntimeHash({
455
+ requestReceiptId: signature.signatureHash,
456
+ producerReceiptId: result.receipt.receiptId,
457
+ }).slice(0, 32)}`;
458
+ return {
459
+ schemaVersion: 'retrieval-leg-receipt.v1',
460
+ legId,
461
+ parentReceiptIds: [signature.receiptId, result.receipt.receiptId],
462
+ channel: signature.channel,
463
+ queryVariant: signature.queryVariant,
464
+ required: signature.queryVariant.kind === 'original',
465
+ status: failureStatus ?? executionStatus(result),
466
+ availability: result.availability,
467
+ candidateCount: result.candidates.length,
468
+ gapCount: result.gaps.length,
469
+ ...(result.availability === 'unavailable'
470
+ ? { reasonCode: result.gaps[0]?.reason ?? 'unavailable' }
471
+ : {}),
472
+ producerReceiptHash: result.receipt.receiptHash,
473
+ };
474
+ }
475
+ function executionStatus(result) {
476
+ const rawCandidateCount = producerCandidateCount(result);
477
+ if (result.availability === 'available') {
478
+ if (result.candidates.length === 0 && rawCandidateCount > 0)
479
+ return 'all_candidates_ineligible';
480
+ return result.candidates.length > 0 ? 'executed' : 'no_candidates';
481
+ }
482
+ if (result.availability === 'empty_valid') {
483
+ return rawCandidateCount > 0 ? 'all_candidates_ineligible' : 'no_candidates';
484
+ }
485
+ return 'unavailable';
486
+ }
487
+ function producerCandidateCount(result) {
488
+ const producerLegs = result.receipt.producerLegs ?? [];
489
+ return Math.max(result.candidates.length, ...producerLegs.map(leg => leg.candidateCount));
490
+ }
491
+ function retrievalExecutionStatus(input) {
492
+ if (input.rerankerFailed || input.legs.some(leg => leg.required && leg.status === 'failed'))
493
+ return 'failed';
494
+ if (input.legs.some(leg => leg.required && leg.status === 'unavailable'))
495
+ return 'unavailable';
496
+ if (input.rawCandidateCount > 0 && input.eligibleCount === 0)
497
+ return 'all_candidates_ineligible';
498
+ if (input.rawCandidateCount === 0)
499
+ return 'no_candidates';
500
+ return 'executed';
501
+ }
502
+ function providerFailureReason(error) {
503
+ const message = error instanceof Error ? error.message.toLowerCase() : '';
504
+ if (message.includes('timeout'))
505
+ return 'producer_timeout';
506
+ if (message.includes('invalid') || message.includes('mismatch'))
507
+ return 'producer_invalid';
508
+ return 'producer_exception';
509
+ }
510
+ function stopReason(cycle, remainingTokenBudget) {
511
+ if (cycle.sufficiency === 'sufficient')
512
+ return 'sufficient';
513
+ if (cycle.sufficiency === 'gap_unavailable')
514
+ return 'required_channel_unavailable';
515
+ if (cycle.sufficiency === 'abstain')
516
+ return 'abstain';
517
+ if (cycle.noGainCount >= 2)
518
+ return 'two_consecutive_no_gain_cycles';
519
+ if (cycle.cycle >= 3)
520
+ return 'maximum_cycles_reached';
521
+ if (remainingTokenBudget === 0)
522
+ return 'token_budget_exhausted';
523
+ return 'gap_actionable';
524
+ }
525
+ function hardFilter(candidates, requestedChannel, context, rejected, now) {
526
+ return candidates.filter(candidate => {
527
+ const vectorCandidate = candidate.contributingLegs.some(leg => /vector|embedding/iu.test(leg.leg));
528
+ const purpose = candidate.channel === 'code' ? 'code_retrieval'
529
+ : candidate.channel === 'memory' ? 'memory_retrieval' : 'knowledge_retrieval';
530
+ const expectedProfile = context.embeddingProfiles[purpose];
531
+ const allowedScopes = allowedScopeKeys(requestedChannel, context);
532
+ const minimumAuthority = requestedChannel === 'knowledge'
533
+ ? context.plan.knowledge.requirement === 'prohibited'
534
+ ? 0 : context.plan.knowledge.typedQuery.minimumAuthority ?? 0
535
+ : 0;
536
+ const scopeKey = `${candidate.eligibility.scope.kind}:${candidate.eligibility.scope.id}`;
537
+ const reason = candidate.channel !== requestedChannel ? 'producer_channel_mismatch'
538
+ : !allowedScopes.has(scopeKey) ? 'scope_mismatch'
539
+ : candidate.eligibility.permission !== 'allowed'
540
+ ? candidate.eligibility.permission === 'denied' ? 'permission_denied' : 'permission_unknown'
541
+ : candidate.eligibility.sourceReady !== true ? 'source_not_ready'
542
+ : candidate.eligibility.expiresAt !== undefined && candidate.eligibility.expiresAt <= now ? 'expired'
543
+ : requestedChannel === 'knowledge' && candidate.eligibility.authority === undefined && minimumAuthority > 0
544
+ ? 'authority_missing'
545
+ : requestedChannel === 'knowledge' && (candidate.eligibility.authority ?? 0) < minimumAuthority
546
+ ? 'authority_below_threshold'
547
+ : vectorCandidate && !expectedProfile ? 'profile_unbound'
548
+ : vectorCandidate && !candidate.eligibility.profileHash ? 'profile_missing'
549
+ : vectorCandidate && candidate.eligibility.profileHash !== expectedProfile ? 'profile_mismatch'
550
+ : !candidate.eligibility.sourceReady ? 'source_not_ready'
551
+ : undefined;
552
+ if (reason)
553
+ rejected.push({ ref: candidate.stableRef, reason });
554
+ return !reason;
555
+ });
556
+ }
557
+ function scopeCandidateLegs(signature, candidate) {
558
+ const legs = candidate.contributingLegs.length > 0
559
+ ? candidate.contributingLegs
560
+ : [{
561
+ leg: candidate.channel,
562
+ rank: candidate.rank,
563
+ ...(candidate.score !== undefined ? { score: candidate.score } : {}),
564
+ }];
565
+ return {
566
+ ...candidate,
567
+ contributingLegs: legs.map(leg => ({
568
+ ...leg,
569
+ leg: `${signature.signatureHash}:${leg.leg}`,
570
+ })),
571
+ };
572
+ }
573
+ function unavailable(channel, context, reason, now, signature) {
574
+ const gap = {
575
+ kind: 'unavailable_channel', reason, evidenceRefs: [], resolver: channel, priority: 1,
576
+ };
577
+ const body = {
578
+ schemaVersion: 'evidence-receipt.v1',
579
+ receiptId: `unavailable:${channel}:${stableRuntimeHash({ context: context.contextHash, reason }).slice(0, 24)}`,
580
+ parentReceiptIds: signature ? [signature.receiptId] : [],
581
+ taskContextHash: context.contextHash,
582
+ channel,
583
+ availability: 'unavailable',
584
+ selectedRefs: [], rejectedRefs: [], gaps: [gap],
585
+ ...(signature ? { legId: signature.signatureHash } : {}),
586
+ factsHash: context.facts.factsHash,
587
+ planHash: context.plan.planHash,
588
+ activationSnapshotHash: context.activationSnapshotHash,
589
+ createdAt: now,
590
+ };
591
+ return { availability: 'unavailable', candidates: [], gaps: [gap], receipt: { ...body, receiptHash: stableRuntimeHash(body) } };
592
+ }
593
+ function providerReceipt(input) {
594
+ const executionId = `provider-execution:${stableRuntimeHash(input.executionKey).slice(0, 32)}`;
595
+ const body = {
596
+ schemaVersion: 'provider-execution-receipt.v1',
597
+ receiptId: `provider-execution-receipt:${stableRuntimeHash(executionId).slice(0, 32)}`,
598
+ executionId,
599
+ requestHash: input.requestHash,
600
+ providerId: input.providerId,
601
+ providerVersion: input.providerVersion,
602
+ capability: 'reranking',
603
+ status: input.status,
604
+ proposalCount: input.proposalCount,
605
+ acceptedCount: input.acceptedCount,
606
+ rejectedCount: input.rejectedCount,
607
+ reasonCodes: [...new Set(input.reasonCodes)].sort(),
608
+ latencyMs: Math.max(0, Math.round(input.latencyMs)),
609
+ createdAt: input.createdAt,
610
+ };
611
+ return { ...body, receiptHash: stableRuntimeHash(body) };
612
+ }
613
+ function providerLogData(receipt) {
614
+ return {
615
+ executionId: receipt.executionId,
616
+ requestHash: receipt.requestHash,
617
+ providerIdHash: stableRuntimeHash(receipt.providerId),
618
+ providerVersionHash: stableRuntimeHash(receipt.providerVersion),
619
+ providerReceiptId: receipt.receiptId,
620
+ providerReceiptHash: receipt.receiptHash,
621
+ capability: receipt.capability,
622
+ status: receipt.status,
623
+ proposalCount: receipt.proposalCount,
624
+ acceptedCount: receipt.acceptedCount,
625
+ rejectedCount: receipt.rejectedCount,
626
+ reasonCodes: receipt.reasonCodes,
627
+ latencyMs: receipt.latencyMs,
628
+ };
629
+ }
630
+ function validateProducerResult(signature, context, result) {
631
+ const channel = signature.channel;
632
+ const { receiptHash, ...receiptBody } = result.receipt;
633
+ if (stableRuntimeHash(receiptBody) !== receiptHash
634
+ || result.receipt.schemaVersion !== 'evidence-receipt.v1'
635
+ || result.receipt.legId !== signature.signatureHash
636
+ || !result.receipt.parentReceiptIds.includes(signature.receiptId)
637
+ || result.receipt.taskContextHash !== context.contextHash
638
+ || result.receipt.channel !== channel
639
+ || result.receipt.availability !== result.availability
640
+ || result.receipt.factsHash !== context.facts.factsHash
641
+ || result.receipt.planHash !== context.plan.planHash
642
+ || result.receipt.activationSnapshotHash !== context.activationSnapshotHash
643
+ || stableRuntimeHash(result.receipt.gaps) !== stableRuntimeHash(result.gaps)) {
644
+ throw new Error('PRODUCER_RECEIPT_INVALID');
645
+ }
646
+ const producerLegs = result.receipt.producerLegs ?? [];
647
+ const requiredLexical = producerLegs.find(leg => leg.kind === 'lexical' && leg.required);
648
+ const requiredVector = producerLegs.find(leg => leg.kind === 'vector' && leg.required);
649
+ const successfulLegStatuses = new Set(['ready', 'empty_valid']);
650
+ if (producerLegs.some(leg => leg.schemaVersion !== 'evidence-producer-leg-receipt.v1'
651
+ || !leg.parentReceiptIds.includes(signature.receiptId))
652
+ || !requiredLexical
653
+ || (signature.filters.embeddingProfileHash && !requiredVector)
654
+ || (result.availability !== 'unavailable'
655
+ && (!successfulLegStatuses.has(requiredLexical.status)
656
+ || (requiredVector && !successfulLegStatuses.has(requiredVector.status))))) {
657
+ throw new Error('PRODUCER_REQUIRED_LEG_RECEIPT_INVALID');
658
+ }
659
+ if ((result.availability === 'unavailable' || result.availability === 'empty_valid')
660
+ && result.candidates.length > 0)
661
+ throw new Error('PRODUCER_AVAILABILITY_CONFLICT');
662
+ if (result.availability === 'unavailable'
663
+ && !result.gaps.some(gap => gap.kind === 'unavailable_channel')) {
664
+ throw new Error('PRODUCER_UNAVAILABLE_GAP_MISSING');
665
+ }
666
+ for (const candidate of result.candidates) {
667
+ if (!candidate.stableRef || candidate.channel !== channel
668
+ || !Number.isSafeInteger(candidate.rank) || candidate.rank < 1
669
+ || !candidate.eligibility
670
+ || !candidate.eligibility.scope?.id
671
+ || !['project', 'session', 'skill'].includes(candidate.eligibility.scope.kind)
672
+ || !['allowed', 'denied', 'unknown'].includes(candidate.eligibility.permission)
673
+ || typeof candidate.eligibility.sourceReady !== 'boolean'
674
+ || (candidate.eligibility.authority !== undefined
675
+ && (!Number.isFinite(candidate.eligibility.authority)
676
+ || candidate.eligibility.authority < 0 || candidate.eligibility.authority > 1))) {
677
+ throw new Error('PRODUCER_CANDIDATE_INVALID');
678
+ }
679
+ stableRuntimeHash(candidate);
680
+ }
681
+ return result;
682
+ }
683
+ function allowedScopeKeys(channel, context) {
684
+ const kinds = channel === 'memory'
685
+ ? new Set(context.plan.memory.requirement === 'prohibited' ? [] : context.plan.memory.typedQuery.scopes)
686
+ : new Set(['project']);
687
+ return new Set([context.scopeBindings.project, context.scopeBindings.session, context.scopeBindings.skill]
688
+ .filter((scope) => Boolean(scope) && kinds.has(scope.kind))
689
+ .map(scope => `${scope.kind}:${scope.id}`));
690
+ }
691
+ function dedupeStrings(values) {
692
+ return [...new Set(values)];
693
+ }
694
+ function dedupeRejected(values) {
695
+ return [...new Map(values.map(value => [`${value.ref}\0${value.reason}`, value])).values()];
696
+ }
697
+ function rejectionReasonCounts(values) {
698
+ const counts = {};
699
+ for (const value of values)
700
+ counts[value.reason] = (counts[value.reason] ?? 0) + 1;
701
+ return Object.fromEntries(Object.entries(counts).sort(([left], [right]) => left.localeCompare(right)));
702
+ }
703
+ function mergeContributingLegs(left, right) {
704
+ const result = {};
705
+ for (const source of [left ?? {}, right ?? {}])
706
+ for (const [leg, values] of Object.entries(source)) {
707
+ result[leg] = [...new Map([...(result[leg] ?? []), ...values]
708
+ .map(value => [`${value.ref}\0${value.rank}`, value])).values()];
709
+ }
710
+ return result;
711
+ }
712
+ function mergeSelectedRefsByChannel(left, right) {
713
+ return Object.fromEntries(['code', 'memory', 'knowledge'].map(channel => [
714
+ channel,
715
+ dedupeStrings([...(left?.[channel] ?? []), ...(right?.[channel] ?? [])]),
716
+ ]));
717
+ }
718
+ function mergeSelectedEvidence(left, right) {
719
+ return [...new Map([...(left ?? []), ...(right ?? [])]
720
+ .map(candidate => [candidate.stableRef, candidate])).values()];
721
+ }
722
+ function mergeLegOutcomes(left, right) {
723
+ return [...new Map([...(left ?? []), ...right].map(outcome => [outcome.legId, outcome])).values()];
724
+ }
725
+ function evidenceStageReceipt(candidate, stage, parentReceiptIds, createdAt, reasonCode, rank) {
726
+ const parents = dedupeStrings(parentReceiptIds).sort();
727
+ const body = {
728
+ schemaVersion: 'evidence-stage-receipt.v1',
729
+ receiptId: `evidence-stage:${stableRuntimeHash({
730
+ stableRef: candidate.stableRef,
731
+ stage,
732
+ parents,
733
+ ...(reasonCode ? { reasonCode } : {}),
734
+ ...(rank !== undefined ? { rank } : {}),
735
+ createdAt,
736
+ }).slice(0, 32)}`,
737
+ parentReceiptIds: parents,
738
+ stableRef: candidate.stableRef,
739
+ channel: candidate.channel,
740
+ stage,
741
+ ...(rank !== undefined ? { rank } : {}),
742
+ ...(reasonCode ? { reasonCode } : {}),
743
+ createdAt,
744
+ };
745
+ return { ...body, receiptHash: stableRuntimeHash(body) };
746
+ }
747
+ function evidenceStageReceiptFrom(parent, stage, createdAt, reasonCode) {
748
+ return evidenceStageReceipt({
749
+ stableRef: parent.stableRef,
750
+ channel: parent.channel,
751
+ }, stage, [parent.receiptId], createdAt, reasonCode);
752
+ }
753
+ function evidenceReceiptIdsByRef(receipts) {
754
+ const result = new Map();
755
+ for (const receipt of receipts) {
756
+ result.set(receipt.stableRef, [
757
+ ...(result.get(receipt.stableRef) ?? []),
758
+ receipt.receiptId,
759
+ ]);
760
+ }
761
+ return result;
762
+ }
763
+ function dedupeEvidenceStageReceipts(receipts) {
764
+ return [...new Map(receipts.map(receipt => [receipt.receiptId, receipt])).values()];
765
+ }
766
+ function requiresStrongCodeEvidence(context) {
767
+ return context.actionAuthority.mutation === 'authorized'
768
+ || context.actionAuthority.mutation === 'conditional';
769
+ }
770
+ function executableChannelCount(context) {
771
+ return [context.plan.code, context.plan.memory, context.plan.knowledge]
772
+ .filter(plan => plan.requirement !== 'prohibited').length;
773
+ }
774
+ function dedupeGaps(gaps) {
775
+ return [...new Map(gaps.map(gap => [stableRuntimeHash(gap), gap])).values()]
776
+ .sort((left, right) => left.priority - right.priority || left.kind.localeCompare(right.kind));
777
+ }
778
+ //# sourceMappingURL=retrieval-coordinator.js.map