@gmickel/gno 1.16.0 → 1.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 (86) hide show
  1. package/README.md +34 -18
  2. package/assets/skill/SKILL.md +25 -6
  3. package/assets/skill/mcp-reference.md +21 -0
  4. package/package.json +2 -2
  5. package/src/app/context-agent-projection.ts +303 -0
  6. package/src/app/context-format.ts +249 -0
  7. package/src/app/context-runtime-contract.ts +325 -0
  8. package/src/app/context-runtime-input.ts +362 -0
  9. package/src/app/context-runtime-types.ts +65 -0
  10. package/src/app/context-runtime.ts +170 -0
  11. package/src/app/context-surface.ts +145 -0
  12. package/src/cli/commands/context-build.ts +149 -0
  13. package/src/cli/commands/context-verify.ts +90 -0
  14. package/src/cli/commands/daemon.ts +69 -2
  15. package/src/cli/commands/models/pull.ts +13 -3
  16. package/src/cli/commands/status.ts +2 -0
  17. package/src/cli/detach.ts +37 -20
  18. package/src/cli/options.ts +4 -0
  19. package/src/cli/program.ts +252 -27
  20. package/src/config/index.ts +3 -0
  21. package/src/config/types.ts +37 -0
  22. package/src/core/context-budget.ts +461 -0
  23. package/src/core/context-capsule-index-schema.ts +15 -0
  24. package/src/core/context-capsule-retrieval-schema.ts +81 -0
  25. package/src/core/context-capsule-schema.ts +473 -0
  26. package/src/core/context-capsule-validation.ts +416 -0
  27. package/src/core/context-capsule-verification.ts +218 -0
  28. package/src/core/context-capsule.ts +439 -0
  29. package/src/core/context-compiler.ts +513 -0
  30. package/src/core/context-evidence-metadata.ts +33 -0
  31. package/src/core/context-evidence.ts +495 -0
  32. package/src/core/context-facets.ts +163 -0
  33. package/src/core/context-guidance.ts +69 -0
  34. package/src/core/context-scope.ts +32 -0
  35. package/src/core/context-verifier-canonical.ts +90 -0
  36. package/src/core/context-verifier-input.ts +66 -0
  37. package/src/core/context-verifier.ts +447 -0
  38. package/src/core/job-manager.ts +19 -0
  39. package/src/core/mutation-generations.ts +33 -0
  40. package/src/core/sections.ts +63 -0
  41. package/src/llm/cache.ts +13 -3
  42. package/src/llm/nodeLlamaCpp/adapter.ts +10 -1
  43. package/src/llm/nodeLlamaCpp/lifecycle.ts +71 -0
  44. package/src/mcp/context.ts +161 -0
  45. package/src/mcp/http-security.ts +477 -0
  46. package/src/mcp/http-session.ts +272 -0
  47. package/src/mcp/http-transport.ts +370 -0
  48. package/src/mcp/resources/index.ts +141 -134
  49. package/src/mcp/server.ts +28 -82
  50. package/src/mcp/tools/add-collection.ts +3 -1
  51. package/src/mcp/tools/capture.ts +3 -0
  52. package/src/mcp/tools/clear-collection-embeddings.ts +2 -0
  53. package/src/mcp/tools/context.ts +230 -0
  54. package/src/mcp/tools/embed.ts +62 -52
  55. package/src/mcp/tools/index-cmd.ts +88 -74
  56. package/src/mcp/tools/index.ts +49 -2
  57. package/src/mcp/tools/remove-collection.ts +2 -0
  58. package/src/mcp/tools/status.ts +11 -0
  59. package/src/mcp/tools/sync.ts +16 -14
  60. package/src/mcp/tools/workspace-write.ts +7 -3
  61. package/src/pipeline/chunk-lookup.ts +33 -0
  62. package/src/pipeline/hybrid.ts +79 -57
  63. package/src/pipeline/types.ts +14 -0
  64. package/src/sdk/client.ts +68 -6
  65. package/src/sdk/index.ts +21 -0
  66. package/src/sdk/types.ts +24 -0
  67. package/src/serve/background-runtime.ts +12 -211
  68. package/src/serve/context-capsule.ts +136 -0
  69. package/src/serve/context.ts +10 -1
  70. package/src/serve/embed-scheduler.ts +74 -43
  71. package/src/serve/index.ts +9 -0
  72. package/src/serve/jobs.ts +78 -80
  73. package/src/serve/public/components/HealthCenter.tsx +74 -1
  74. package/src/serve/public/globals.built.css +1 -1
  75. package/src/serve/public/pages/Dashboard.tsx +1 -0
  76. package/src/serve/resident-admission.ts +159 -0
  77. package/src/serve/resident-background-work.ts +39 -0
  78. package/src/serve/resident-request.ts +55 -0
  79. package/src/serve/resident-runtime.ts +490 -0
  80. package/src/serve/resident-status.ts +96 -0
  81. package/src/serve/routes/api.ts +265 -167
  82. package/src/serve/routes/mcp.ts +69 -0
  83. package/src/serve/server.ts +212 -35
  84. package/src/serve/status-model.ts +51 -0
  85. package/src/serve/status.ts +5 -0
  86. package/src/store/sqlite/adapter.ts +64 -29
@@ -0,0 +1,461 @@
1
+ /**
2
+ * Deterministic evidence selection under one canonical payload budget.
3
+ *
4
+ * Candidate text is already materialized into its final extractive form. The
5
+ * caller-owned projector is the budget authority: it must build the complete
6
+ * canonical Capsule payload, including coverage, omissions, and guidance.
7
+ */
8
+
9
+ export const CONTEXT_OMISSION_REASONS = [
10
+ "duplicate",
11
+ "overlap",
12
+ "global_budget",
13
+ "redundant_coverage",
14
+ "document_share_cap",
15
+ "filtered_by_scope",
16
+ "invalid_coordinates",
17
+ ] as const;
18
+
19
+ export type ContextOmissionReason = (typeof CONTEXT_OMISSION_REASONS)[number];
20
+
21
+ export type ContextGapReason =
22
+ | "facet_not_found"
23
+ | "global_budget_exhausted"
24
+ | "filtered_by_scope";
25
+
26
+ export interface ContextCandidateReference {
27
+ candidateId: string;
28
+ uri: string;
29
+ docid: string;
30
+ startLine: number | null;
31
+ endLine: number | null;
32
+ passageHash: string | null;
33
+ sourceHash: string;
34
+ mirrorHash: string;
35
+ }
36
+
37
+ export interface MaterializedContextCandidate<
38
+ T = unknown,
39
+ > extends ContextCandidateReference {
40
+ startLine: number;
41
+ endLine: number;
42
+ passageHash: string;
43
+ text: string;
44
+ facets: string[];
45
+ retrievalRank: number;
46
+ value: T;
47
+ }
48
+
49
+ export interface ContextOmission extends ContextCandidateReference {
50
+ reason: ContextOmissionReason;
51
+ }
52
+
53
+ export interface ContextReasonCounts {
54
+ duplicate: number;
55
+ overlap: number;
56
+ global_budget: number;
57
+ redundant_coverage: number;
58
+ document_share_cap: number;
59
+ filtered_by_scope: number;
60
+ invalid_coordinates: number;
61
+ }
62
+
63
+ export interface ContextCoverageState {
64
+ coveredFacets: string[];
65
+ unresolvedFacets: string[];
66
+ gaps: Array<{ facet: string; code: ContextGapReason }>;
67
+ }
68
+
69
+ export interface ContextSelectionState<T = unknown> {
70
+ selected: MaterializedContextCandidate<T>[];
71
+ omissions: ContextOmission[];
72
+ reasonCounts: ContextReasonCounts;
73
+ coverage: ContextCoverageState;
74
+ }
75
+
76
+ export interface ContextCanonicalProjection<T = unknown> {
77
+ value: T;
78
+ usedBytes: number;
79
+ usedTokens: number;
80
+ }
81
+
82
+ export interface ContextBudgetLimits {
83
+ requestedBytes: number;
84
+ requestedTokens: number;
85
+ safetyMarginBytes: number;
86
+ safetyMarginTokens: number;
87
+ /** Defaults to 3/5 of the spendable byte budget. */
88
+ documentShareNumerator?: number;
89
+ documentShareDenominator?: number;
90
+ }
91
+
92
+ export interface ContextSelectionOptions<T, P> {
93
+ candidates: MaterializedContextCandidate<T>[];
94
+ requestedFacets: string[];
95
+ initialOmissions?: ContextOmission[];
96
+ filteredFacetMatches?: ReadonlySet<string>;
97
+ limits: ContextBudgetLimits;
98
+ projectCanonical: (
99
+ state: ContextSelectionState<T>
100
+ ) => ContextCanonicalProjection<P> | null;
101
+ }
102
+
103
+ export interface ContextSelectionResult<T, P> extends ContextSelectionState<T> {
104
+ projection: ContextCanonicalProjection<P> | null;
105
+ }
106
+
107
+ const compareCodeUnits = (left: string, right: string): number => {
108
+ if (left < right) return -1;
109
+ if (left > right) return 1;
110
+ return 0;
111
+ };
112
+
113
+ const compareReferences = (
114
+ left: ContextCandidateReference,
115
+ right: ContextCandidateReference
116
+ ): number =>
117
+ compareCodeUnits(left.uri, right.uri) ||
118
+ (left.startLine ?? 0) - (right.startLine ?? 0) ||
119
+ (left.endLine ?? 0) - (right.endLine ?? 0) ||
120
+ compareCodeUnits(left.sourceHash, right.sourceHash) ||
121
+ compareCodeUnits(left.candidateId, right.candidateId);
122
+
123
+ const compareOmissions = (
124
+ left: ContextOmission,
125
+ right: ContextOmission
126
+ ): number =>
127
+ compareCodeUnits(left.reason, right.reason) || compareReferences(left, right);
128
+
129
+ const emptyReasonCounts = (): ContextReasonCounts => ({
130
+ duplicate: 0,
131
+ overlap: 0,
132
+ global_budget: 0,
133
+ redundant_coverage: 0,
134
+ document_share_cap: 0,
135
+ filtered_by_scope: 0,
136
+ invalid_coordinates: 0,
137
+ });
138
+
139
+ const dedupeOmissions = (omissions: ContextOmission[]): ContextOmission[] => {
140
+ const byId = new Map<string, ContextOmission>();
141
+ for (const omission of [...omissions].sort(compareOmissions)) {
142
+ if (!byId.has(omission.candidateId)) {
143
+ byId.set(omission.candidateId, omission);
144
+ }
145
+ }
146
+ return [...byId.values()].sort(compareOmissions);
147
+ };
148
+
149
+ const countReasons = (omissions: ContextOmission[]): ContextReasonCounts => {
150
+ const counts = emptyReasonCounts();
151
+ for (const omission of omissions) counts[omission.reason] += 1;
152
+ return counts;
153
+ };
154
+
155
+ const coveredFacetSet = <T>(
156
+ selected: MaterializedContextCandidate<T>[]
157
+ ): Set<string> => new Set(selected.flatMap((candidate) => candidate.facets));
158
+
159
+ const buildCoverage = <T>(
160
+ requestedFacets: string[],
161
+ selected: MaterializedContextCandidate<T>[],
162
+ candidates: MaterializedContextCandidate<T>[],
163
+ filteredFacetMatches: ReadonlySet<string>
164
+ ): ContextCoverageState => {
165
+ const covered = coveredFacetSet(selected);
166
+ const available = new Set(
167
+ candidates.flatMap((candidate) => candidate.facets)
168
+ );
169
+ const coveredFacets = requestedFacets.filter((facet) => covered.has(facet));
170
+ const unresolvedFacets = requestedFacets.filter(
171
+ (facet) => !covered.has(facet)
172
+ );
173
+ const gaps = unresolvedFacets.map((facet) => ({
174
+ facet,
175
+ code: available.has(facet)
176
+ ? ("global_budget_exhausted" as const)
177
+ : filteredFacetMatches.has(facet)
178
+ ? ("filtered_by_scope" as const)
179
+ : ("facet_not_found" as const),
180
+ }));
181
+ return { coveredFacets, unresolvedFacets, gaps };
182
+ };
183
+
184
+ const buildState = <T>(
185
+ requestedFacets: string[],
186
+ selected: MaterializedContextCandidate<T>[],
187
+ omissions: ContextOmission[],
188
+ candidates: MaterializedContextCandidate<T>[],
189
+ filteredFacetMatches: ReadonlySet<string>
190
+ ): ContextSelectionState<T> => {
191
+ const orderedOmissions = dedupeOmissions(omissions);
192
+ return {
193
+ selected: selected.map((candidate) => ({ ...candidate })),
194
+ omissions: orderedOmissions,
195
+ reasonCounts: countReasons(orderedOmissions),
196
+ coverage: buildCoverage(
197
+ requestedFacets,
198
+ selected,
199
+ candidates,
200
+ filteredFacetMatches
201
+ ),
202
+ };
203
+ };
204
+
205
+ const overlaps = <T>(
206
+ candidate: MaterializedContextCandidate<T>,
207
+ selected: MaterializedContextCandidate<T>[]
208
+ ): boolean =>
209
+ selected.some(
210
+ (item) =>
211
+ item.docid === candidate.docid &&
212
+ candidate.startLine <= item.endLine &&
213
+ item.startLine <= candidate.endLine
214
+ );
215
+
216
+ const utf8Bytes = (value: string): number =>
217
+ new TextEncoder().encode(value).byteLength;
218
+
219
+ const validateLimits = (limits: ContextBudgetLimits): void => {
220
+ const values = [
221
+ limits.requestedBytes,
222
+ limits.requestedTokens,
223
+ limits.safetyMarginBytes,
224
+ limits.safetyMarginTokens,
225
+ ];
226
+ if (
227
+ values.some((value) => !Number.isSafeInteger(value) || value < 0) ||
228
+ limits.requestedBytes <= limits.safetyMarginBytes ||
229
+ limits.requestedTokens <= limits.safetyMarginTokens
230
+ ) {
231
+ throw new Error("Context budget limits must be positive safe integers");
232
+ }
233
+ };
234
+
235
+ const projectionFits = <P>(
236
+ projection: ContextCanonicalProjection<P> | null,
237
+ limits: ContextBudgetLimits
238
+ ): projection is ContextCanonicalProjection<P> =>
239
+ projection !== null &&
240
+ Number.isSafeInteger(projection.usedBytes) &&
241
+ Number.isSafeInteger(projection.usedTokens) &&
242
+ projection.usedBytes >= 0 &&
243
+ projection.usedTokens >= 0 &&
244
+ projection.usedBytes + limits.safetyMarginBytes <= limits.requestedBytes &&
245
+ projection.usedTokens + limits.safetyMarginTokens <= limits.requestedTokens;
246
+
247
+ const documentShareExceeded = <T>(
248
+ candidate: MaterializedContextCandidate<T>,
249
+ selected: MaterializedContextCandidate<T>[],
250
+ candidateDocumentCount: number,
251
+ limits: ContextBudgetLimits
252
+ ): boolean => {
253
+ if (candidateDocumentCount <= 1) return false;
254
+ const numerator = limits.documentShareNumerator ?? 3;
255
+ const denominator = limits.documentShareDenominator ?? 5;
256
+ if (
257
+ !Number.isSafeInteger(numerator) ||
258
+ !Number.isSafeInteger(denominator) ||
259
+ numerator < 1 ||
260
+ denominator < numerator
261
+ ) {
262
+ throw new Error("Invalid Context document share ratio");
263
+ }
264
+ const spendableBytes = limits.requestedBytes - limits.safetyMarginBytes;
265
+ const shareLimit = Number(
266
+ (BigInt(spendableBytes) * BigInt(numerator)) / BigInt(denominator)
267
+ );
268
+ const sameDocumentBytes = selected
269
+ .filter((item) => item.docid === candidate.docid)
270
+ .reduce((sum, item) => sum + utf8Bytes(item.text), 0);
271
+ return sameDocumentBytes + utf8Bytes(candidate.text) > shareLimit;
272
+ };
273
+
274
+ const omissionReason = <T>(
275
+ candidate: MaterializedContextCandidate<T>,
276
+ selected: MaterializedContextCandidate<T>[],
277
+ covered: ReadonlySet<string>,
278
+ requestedFacetCount: number,
279
+ candidateDocumentCount: number,
280
+ limits: ContextBudgetLimits
281
+ ): ContextOmissionReason | null => {
282
+ if (requestedFacetCount > 0 && candidate.facets.length === 0) {
283
+ return "redundant_coverage";
284
+ }
285
+ if (overlaps(candidate, selected)) return "overlap";
286
+ if (
287
+ documentShareExceeded(candidate, selected, candidateDocumentCount, limits)
288
+ ) {
289
+ return "document_share_cap";
290
+ }
291
+ if (
292
+ selected.length > 0 &&
293
+ candidate.facets.every((facet) => covered.has(facet))
294
+ ) {
295
+ return "redundant_coverage";
296
+ }
297
+ return null;
298
+ };
299
+
300
+ const asOmission = <T>(
301
+ candidate: MaterializedContextCandidate<T>,
302
+ reason: ContextOmissionReason
303
+ ): ContextOmission => ({
304
+ candidateId: candidate.candidateId,
305
+ uri: candidate.uri,
306
+ docid: candidate.docid,
307
+ startLine: candidate.startLine,
308
+ endLine: candidate.endLine,
309
+ passageHash: candidate.passageHash,
310
+ sourceHash: candidate.sourceHash,
311
+ mirrorHash: candidate.mirrorHash,
312
+ reason,
313
+ });
314
+
315
+ const collapseDuplicates = <T>(
316
+ candidates: MaterializedContextCandidate<T>[]
317
+ ): {
318
+ candidates: MaterializedContextCandidate<T>[];
319
+ omissions: ContextOmission[];
320
+ } => {
321
+ const ordered = [...candidates].sort(
322
+ (left, right) =>
323
+ left.retrievalRank - right.retrievalRank || compareReferences(left, right)
324
+ );
325
+ const seenIds = new Set<string>();
326
+ const seenPassages = new Set<string>();
327
+ const kept: MaterializedContextCandidate<T>[] = [];
328
+ const omissions: ContextOmission[] = [];
329
+ for (const candidate of ordered) {
330
+ if (seenIds.has(candidate.candidateId)) continue;
331
+ seenIds.add(candidate.candidateId);
332
+ if (seenPassages.has(candidate.passageHash)) {
333
+ omissions.push(asOmission(candidate, "duplicate"));
334
+ continue;
335
+ }
336
+ seenPassages.add(candidate.passageHash);
337
+ kept.push(candidate);
338
+ }
339
+ return { candidates: kept, omissions };
340
+ };
341
+
342
+ const compareMarginalValue = <T>(
343
+ left: MaterializedContextCandidate<T>,
344
+ right: MaterializedContextCandidate<T>,
345
+ covered: ReadonlySet<string>,
346
+ poolSize: number
347
+ ): number => {
348
+ const uncovered = (candidate: MaterializedContextCandidate<T>): number =>
349
+ candidate.facets.filter((facet) => !covered.has(facet)).length;
350
+ const leftUncovered = uncovered(left);
351
+ const rightUncovered = uncovered(right);
352
+ const leftCost = BigInt(Math.max(1, utf8Bytes(left.text)));
353
+ const rightCost = BigInt(Math.max(1, utf8Bytes(right.text)));
354
+ const leftRatio = BigInt(leftUncovered) * rightCost;
355
+ const rightRatio = BigInt(rightUncovered) * leftCost;
356
+ if (leftRatio !== rightRatio) return leftRatio > rightRatio ? -1 : 1;
357
+ const relevance = (candidate: MaterializedContextCandidate<T>): number =>
358
+ Math.max(1, poolSize - candidate.retrievalRank + 1);
359
+ const relevanceDifference = relevance(right) - relevance(left);
360
+ if (relevanceDifference !== 0) return relevanceDifference;
361
+ if (left.retrievalRank !== right.retrievalRank) {
362
+ return left.retrievalRank - right.retrievalRank;
363
+ }
364
+ return compareReferences(left, right);
365
+ };
366
+
367
+ /** Select materialized candidates using exact full-payload fit checks. */
368
+ export const selectContextEvidence = <T, P>(
369
+ options: ContextSelectionOptions<T, P>
370
+ ): ContextSelectionResult<T, P> => {
371
+ validateLimits(options.limits);
372
+ const requestedFacets = [...new Set(options.requestedFacets)].sort(
373
+ compareCodeUnits
374
+ );
375
+ const filteredFacetMatches =
376
+ options.filteredFacetMatches ?? new Set<string>();
377
+ const collapsed = collapseDuplicates(options.candidates);
378
+ const candidates = collapsed.candidates;
379
+ const candidateDocumentCount = new Set(
380
+ candidates.map((candidate) => candidate.docid)
381
+ ).size;
382
+ const selected: MaterializedContextCandidate<T>[] = [];
383
+ const omissions = [
384
+ ...(options.initialOmissions ?? []),
385
+ ...collapsed.omissions,
386
+ ];
387
+ const remaining = [...candidates];
388
+ let projection: ContextCanonicalProjection<P> | null = null;
389
+
390
+ while (remaining.length > 0) {
391
+ const covered = coveredFacetSet(selected);
392
+ const eligible: MaterializedContextCandidate<T>[] = [];
393
+ for (const candidate of remaining) {
394
+ const reason = omissionReason(
395
+ candidate,
396
+ selected,
397
+ covered,
398
+ requestedFacets.length,
399
+ candidateDocumentCount,
400
+ options.limits
401
+ );
402
+ if (reason) omissions.push(asOmission(candidate, reason));
403
+ else eligible.push(candidate);
404
+ }
405
+ remaining.length = 0;
406
+ remaining.push(...eligible);
407
+ if (remaining.length === 0) break;
408
+
409
+ remaining.sort((left, right) =>
410
+ compareMarginalValue(left, right, covered, candidates.length)
411
+ );
412
+ const candidate = remaining.shift();
413
+ if (!candidate) break;
414
+ const proposedSelected = [...selected, candidate];
415
+ const proposedCovered = coveredFacetSet(proposedSelected);
416
+ const provisionalOmissions = [
417
+ ...omissions,
418
+ ...remaining.map((item) =>
419
+ asOmission(
420
+ item,
421
+ omissionReason(
422
+ item,
423
+ proposedSelected,
424
+ proposedCovered,
425
+ requestedFacets.length,
426
+ candidateDocumentCount,
427
+ options.limits
428
+ ) ?? "global_budget"
429
+ )
430
+ ),
431
+ ];
432
+ const state = buildState(
433
+ requestedFacets,
434
+ proposedSelected,
435
+ provisionalOmissions,
436
+ candidates,
437
+ filteredFacetMatches
438
+ );
439
+ const proposedProjection = options.projectCanonical(state);
440
+ if (projectionFits(proposedProjection, options.limits)) {
441
+ selected.push(candidate);
442
+ projection = proposedProjection;
443
+ } else {
444
+ omissions.push(asOmission(candidate, "global_budget"));
445
+ }
446
+ }
447
+
448
+ const finalState = buildState(
449
+ requestedFacets,
450
+ selected,
451
+ omissions,
452
+ candidates,
453
+ filteredFacetMatches
454
+ );
455
+ const finalProjection =
456
+ selected.length > 0 ? options.projectCanonical(finalState) : null;
457
+ if (selected.length > 0 && !projectionFits(finalProjection, options.limits)) {
458
+ throw new Error("Final canonical Context projection exceeds its budget");
459
+ }
460
+ return { ...finalState, projection: finalProjection ?? projection };
461
+ };
@@ -0,0 +1,15 @@
1
+ import { z } from "zod";
2
+
3
+ const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/);
4
+
5
+ export const contextCapsuleIndexSnapshotSchema = z
6
+ .object({
7
+ before: sha256Schema,
8
+ after: sha256Schema,
9
+ stable: z.literal(true),
10
+ })
11
+ .strict()
12
+ .refine((value) => value.before === value.after, {
13
+ message: "index changed while the operation was running",
14
+ path: ["after"],
15
+ });
@@ -0,0 +1,81 @@
1
+ /** Versioned normalized retrieval request and capability-state schema. */
2
+
3
+ import { z } from "zod";
4
+
5
+ import { contextCapsuleIndexSnapshotSchema } from "./context-capsule-index-schema";
6
+
7
+ const nonEmptyText = z.string().min(1);
8
+ const capabilityOutcomeSchema = z.enum([
9
+ "not_requested",
10
+ "not_attempted",
11
+ "used",
12
+ "unavailable",
13
+ ]);
14
+
15
+ export const contextCapabilityStateSchema = z
16
+ .object({
17
+ requested: z.boolean(),
18
+ attempted: z.boolean(),
19
+ outcome: capabilityOutcomeSchema,
20
+ fallbackReasons: z.array(nonEmptyText.max(256)).max(32),
21
+ })
22
+ .strict()
23
+ .superRefine((value, context) => {
24
+ const valid =
25
+ (value.outcome === "not_requested" &&
26
+ !value.requested &&
27
+ !value.attempted &&
28
+ value.fallbackReasons.length === 0) ||
29
+ (value.outcome === "not_attempted" &&
30
+ value.requested &&
31
+ !value.attempted) ||
32
+ (value.outcome === "used" && value.requested && value.attempted) ||
33
+ (value.outcome === "unavailable" &&
34
+ value.requested &&
35
+ value.attempted &&
36
+ value.fallbackReasons.length > 0);
37
+ if (!valid) {
38
+ context.addIssue({
39
+ code: "custom",
40
+ message: "requested, attempted, outcome, and fallback reasons disagree",
41
+ });
42
+ }
43
+ });
44
+
45
+ const queryModeSchema = z
46
+ .object({
47
+ mode: z.enum(["term", "intent", "hyde"]),
48
+ text: nonEmptyText.max(4096),
49
+ })
50
+ .strict();
51
+
52
+ export const contextCapsuleRetrievalSchema = z
53
+ .object({
54
+ depthPolicy: z.enum(["fast", "balanced", "thorough"]),
55
+ facets: z.array(nonEmptyText.max(512)).max(128),
56
+ queryVariants: z.array(nonEmptyText.max(4096)).min(1).max(128),
57
+ expansionPolicy: z.literal("deterministic_only"),
58
+ request: z
59
+ .object({
60
+ author: z.string().min(1).max(256).nullable(),
61
+ lang: z.string().min(1).max(64).nullable(),
62
+ queryModes: z.array(queryModeSchema).max(128),
63
+ limit: z.number().int().positive(),
64
+ candidateLimit: z.number().int().positive(),
65
+ graphRequested: z.boolean(),
66
+ })
67
+ .strict(),
68
+ capabilityStates: z
69
+ .object({
70
+ semanticSearch: contextCapabilityStateSchema,
71
+ reranking: contextCapabilityStateSchema,
72
+ graphExpansion: contextCapabilityStateSchema,
73
+ })
74
+ .strict(),
75
+ indexSnapshot: contextCapsuleIndexSnapshotSchema,
76
+ })
77
+ .strict();
78
+
79
+ export type ContextCapabilityState = z.infer<
80
+ typeof contextCapabilityStateSchema
81
+ >;