@gmickel/gno 1.18.0 → 1.20.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 (153) hide show
  1. package/README.md +14 -7
  2. package/assets/skill/SKILL.md +54 -12
  3. package/assets/skill/mcp-reference.md +7 -2
  4. package/assets/skill/recipes/citation-and-provenance.md +32 -9
  5. package/package.json +2 -1
  6. package/spec/AGENTS.md +83 -0
  7. package/spec/CLAUDE.md +83 -0
  8. package/spec/bench-fixture.schema.json +137 -0
  9. package/spec/cli.md +2919 -0
  10. package/spec/db/schema.sql +442 -0
  11. package/spec/evals-agentic.md +592 -0
  12. package/spec/evals.md +1106 -0
  13. package/spec/mcp.md +2279 -0
  14. package/spec/output-schemas/activation-verification.schema.json +515 -0
  15. package/spec/output-schemas/ask.schema.json +564 -0
  16. package/spec/output-schemas/backlinks.schema.json +131 -0
  17. package/spec/output-schemas/bench-result.schema.json +120 -0
  18. package/spec/output-schemas/capture-receipt.schema.json +143 -0
  19. package/spec/output-schemas/claim-verification.schema.json +291 -0
  20. package/spec/output-schemas/collection-list.schema.json +45 -0
  21. package/spec/output-schemas/context-capsule-v1.schema.json +726 -0
  22. package/spec/output-schemas/context-capsule-verification.schema.json +1338 -0
  23. package/spec/output-schemas/context-list.schema.json +21 -0
  24. package/spec/output-schemas/doctor.schema.json +313 -0
  25. package/spec/output-schemas/error.schema.json +30 -0
  26. package/spec/output-schemas/expansion.schema.json +37 -0
  27. package/spec/output-schemas/get.schema.json +140 -0
  28. package/spec/output-schemas/graph-query.schema.json +99 -0
  29. package/spec/output-schemas/graph.schema.json +371 -0
  30. package/spec/output-schemas/links-list.schema.json +186 -0
  31. package/spec/output-schemas/mcp-add-collection-result.schema.json +23 -0
  32. package/spec/output-schemas/mcp-capture-result.schema.json +152 -0
  33. package/spec/output-schemas/mcp-http-error.schema.json +30 -0
  34. package/spec/output-schemas/mcp-job-list.schema.json +58 -0
  35. package/spec/output-schemas/mcp-job-status.schema.json +224 -0
  36. package/spec/output-schemas/mcp-remove-result.schema.json +39 -0
  37. package/spec/output-schemas/mcp-sync-result.schema.json +41 -0
  38. package/spec/output-schemas/mcp-tag-result.schema.json +33 -0
  39. package/spec/output-schemas/models-list.schema.json +93 -0
  40. package/spec/output-schemas/multi-get.schema.json +103 -0
  41. package/spec/output-schemas/process-status.schema.json +119 -0
  42. package/spec/output-schemas/query-diagnose.schema.json +123 -0
  43. package/spec/output-schemas/resident-status.schema.json +154 -0
  44. package/spec/output-schemas/retrieval-trace-common.schema.json +492 -0
  45. package/spec/output-schemas/retrieval-trace-delete.schema.json +16 -0
  46. package/spec/output-schemas/retrieval-trace-export.schema.json +61 -0
  47. package/spec/output-schemas/retrieval-trace-filters.schema.json +139 -0
  48. package/spec/output-schemas/retrieval-trace-judgment.schema.json +15 -0
  49. package/spec/output-schemas/retrieval-trace-list.schema.json +18 -0
  50. package/spec/output-schemas/retrieval-trace-payloads.schema.json +178 -0
  51. package/spec/output-schemas/retrieval-trace-purge.schema.json +31 -0
  52. package/spec/output-schemas/retrieval-trace-qrels.schema.json +303 -0
  53. package/spec/output-schemas/retrieval-trace-replay.schema.json +286 -0
  54. package/spec/output-schemas/retrieval-trace-show.schema.json +69 -0
  55. package/spec/output-schemas/retrieval-trace-summary.schema.json +65 -0
  56. package/spec/output-schemas/search-result.schema.json +154 -0
  57. package/spec/output-schemas/search-results.schema.json +338 -0
  58. package/spec/output-schemas/similar.schema.json +84 -0
  59. package/spec/output-schemas/status.schema.json +676 -0
  60. package/spec/output-schemas/tags-list.schema.json +48 -0
  61. package/src/app/context-runtime-contract.ts +10 -5
  62. package/src/app/context-runtime-input.ts +29 -1
  63. package/src/app/context-runtime-types.ts +7 -0
  64. package/src/app/context-runtime.ts +20 -2
  65. package/src/app/context-surface.ts +4 -0
  66. package/src/app/verified-ask.ts +291 -0
  67. package/src/cli/commands/ask-format.ts +255 -0
  68. package/src/cli/commands/ask.ts +144 -183
  69. package/src/cli/commands/context-build.ts +56 -9
  70. package/src/cli/commands/get.ts +64 -3
  71. package/src/cli/commands/query.ts +62 -23
  72. package/src/cli/commands/replay.ts +140 -0
  73. package/src/cli/commands/search.ts +48 -3
  74. package/src/cli/commands/shared.ts +3 -1
  75. package/src/cli/commands/trace.ts +200 -0
  76. package/src/cli/commands/vsearch.ts +75 -53
  77. package/src/cli/program.ts +287 -1
  78. package/src/config/index.ts +9 -0
  79. package/src/config/retrieval-traces.ts +56 -0
  80. package/src/config/types.ts +4 -0
  81. package/src/core/context-budget.ts +6 -0
  82. package/src/core/context-capsule-retrieval-schema.ts +4 -0
  83. package/src/core/context-capsule-schema.ts +17 -0
  84. package/src/core/context-capsule-validation.ts +3 -2
  85. package/src/core/context-capsule.ts +18 -0
  86. package/src/core/context-compiler.ts +44 -25
  87. package/src/core/context-evidence.ts +6 -0
  88. package/src/core/retrieval-qrels.ts +405 -0
  89. package/src/core/retrieval-replay-candidate.ts +368 -0
  90. package/src/core/retrieval-replay-types.ts +109 -0
  91. package/src/core/retrieval-replay-validation.ts +89 -0
  92. package/src/core/retrieval-replay.ts +441 -0
  93. package/src/core/retrieval-trace-evidence-origin.ts +178 -0
  94. package/src/core/retrieval-trace-export.ts +113 -0
  95. package/src/core/retrieval-trace-filter-normalization.ts +27 -0
  96. package/src/core/retrieval-trace-filters.ts +19 -0
  97. package/src/core/retrieval-trace-management-helpers.ts +247 -0
  98. package/src/core/retrieval-trace-management-types.ts +132 -0
  99. package/src/core/retrieval-trace-management.ts +422 -0
  100. package/src/core/retrieval-trace-request.ts +141 -0
  101. package/src/core/retrieval-trace-session.ts +507 -0
  102. package/src/core/retrieval-trace.ts +472 -0
  103. package/src/llm/errors.ts +10 -1
  104. package/src/llm/httpGeneration.ts +11 -1
  105. package/src/llm/nodeLlamaCpp/generation.ts +54 -10
  106. package/src/llm/types.ts +6 -0
  107. package/src/mcp/tools/ask.ts +228 -0
  108. package/src/mcp/tools/context.ts +87 -15
  109. package/src/mcp/tools/get.ts +35 -1
  110. package/src/mcp/tools/index.ts +83 -0
  111. package/src/mcp/tools/query.ts +95 -64
  112. package/src/mcp/tools/search.ts +36 -13
  113. package/src/mcp/tools/trace.ts +143 -0
  114. package/src/mcp/tools/vsearch.ts +71 -38
  115. package/src/pipeline/answer.ts +167 -26
  116. package/src/pipeline/claim-verification-schema.ts +235 -0
  117. package/src/pipeline/claim-verification.ts +487 -0
  118. package/src/pipeline/claim-verifier.ts +474 -0
  119. package/src/pipeline/graph-retrieval.ts +15 -1
  120. package/src/pipeline/hybrid.ts +151 -43
  121. package/src/pipeline/search.ts +36 -3
  122. package/src/pipeline/trace-metadata.ts +47 -0
  123. package/src/pipeline/types.ts +68 -0
  124. package/src/pipeline/vsearch.ts +101 -38
  125. package/src/sdk/client.ts +415 -73
  126. package/src/sdk/documents.ts +48 -1
  127. package/src/sdk/index.ts +17 -0
  128. package/src/sdk/types.ts +28 -0
  129. package/src/serve/context-capsule.ts +67 -8
  130. package/src/serve/public/app.tsx +12 -1
  131. package/src/serve/public/components/AskVerificationPanel.tsx +189 -0
  132. package/src/serve/public/globals.built.css +1 -1
  133. package/src/serve/public/lib/workspace-tabs.ts +2 -0
  134. package/src/serve/public/pages/Ask.tsx +42 -4
  135. package/src/serve/public/pages/Dashboard.tsx +10 -0
  136. package/src/serve/public/pages/TraceHistory.tsx +478 -0
  137. package/src/serve/public/pages/trace-history-detail.tsx +224 -0
  138. package/src/serve/retrieval-trace.ts +28 -0
  139. package/src/serve/routes/api.ts +508 -68
  140. package/src/serve/routes/traces.ts +156 -0
  141. package/src/serve/server.ts +87 -2
  142. package/src/store/index.ts +31 -0
  143. package/src/store/migrations/014-retrieval-traces.ts +303 -0
  144. package/src/store/migrations/index.ts +2 -0
  145. package/src/store/retrieval-trace-codec.ts +384 -0
  146. package/src/store/sqlite/adapter.ts +153 -1
  147. package/src/store/sqlite/retrieval-trace-management-store.ts +341 -0
  148. package/src/store/sqlite/retrieval-trace-retention.ts +349 -0
  149. package/src/store/sqlite/retrieval-trace-rows.ts +267 -0
  150. package/src/store/sqlite/retrieval-trace-store.ts +515 -0
  151. package/src/store/types.ts +297 -0
  152. package/src/store/vector/sqlite-vec.ts +76 -1
  153. package/src/store/vector/types.ts +1 -1
@@ -0,0 +1,472 @@
1
+ /** Opt-in, local-only retrieval trace recording with privacy projections. */
2
+
3
+ import { z } from "zod";
4
+
5
+ import type {
6
+ RetrievalTraceConfig,
7
+ RetrievalTraceRedactionMode,
8
+ } from "../config/retrieval-traces";
9
+ import type {
10
+ RetrievalTraceAppendResult,
11
+ RetrievalTraceEventInput,
12
+ RetrievalTraceFingerprints,
13
+ RetrievalTraceJudgmentInput,
14
+ RetrievalTraceRunInput,
15
+ RetrievalTraceTerminalStatus,
16
+ StorePort,
17
+ StoreResult,
18
+ } from "../store/types";
19
+
20
+ import {
21
+ parseRetrievalTraceEventInput,
22
+ parseRetrievalTraceJudgmentInput,
23
+ parseRetrievalTraceRunInput,
24
+ } from "../store/retrieval-trace-codec";
25
+ import { err, ok } from "../store/types";
26
+ import { canonicalizeRetrievalTraceFilters } from "./retrieval-trace-filter-normalization";
27
+
28
+ const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/);
29
+ const gnoUriSchema = z
30
+ .string()
31
+ .max(4096)
32
+ .refine((value) => value.startsWith("gno://"), {
33
+ message: "Evidence URI must use the canonical gno:// reader identity",
34
+ });
35
+ const queryModeSchema = z
36
+ .object({
37
+ mode: z.enum(["term", "intent", "hyde"]),
38
+ text: z.string().max(8192),
39
+ })
40
+ .strict();
41
+ export const traceFiltersSchema = z
42
+ .object({
43
+ limit: z.number().int().positive().optional(),
44
+ minScore: z.number().min(0).max(1).optional(),
45
+ collection: z.string().max(256).optional(),
46
+ collections: z.array(z.string().min(1).max(256)).max(1000).optional(),
47
+ lang: z.string().max(64).optional(),
48
+ full: z.boolean().optional(),
49
+ lineNumbers: z.boolean().optional(),
50
+ tagsAll: z.array(z.string().max(512)).max(1000).optional(),
51
+ tagsAny: z.array(z.string().max(512)).max(1000).optional(),
52
+ since: z.string().max(128).optional(),
53
+ until: z.string().max(128).optional(),
54
+ categories: z.array(z.string().max(512)).max(1000).optional(),
55
+ author: z.string().max(1024).optional(),
56
+ intent: z.string().max(8192).optional(),
57
+ exclude: z.array(z.string().max(1024)).max(1000).optional(),
58
+ noExpand: z.boolean().optional(),
59
+ noRerank: z.boolean().optional(),
60
+ candidateLimit: z.number().int().positive().optional(),
61
+ explain: z.boolean().optional(),
62
+ graph: z.boolean().optional(),
63
+ noGraph: z.boolean().optional(),
64
+ queryLanguageHint: z.string().max(64).optional(),
65
+ queryModes: z.array(queryModeSchema).max(100).optional(),
66
+ uriPrefix: z.string().max(4096).optional(),
67
+ })
68
+ .strict();
69
+ const startTraceSchema = z
70
+ .object({
71
+ traceId: z.string().min(1).max(128).optional(),
72
+ query: z.string().min(1).max(8192),
73
+ goal: z.string().max(8192).optional(),
74
+ filters: traceFiltersSchema.default({}),
75
+ fingerprints: z
76
+ .object({
77
+ pipeline: sha256Schema,
78
+ model: sha256Schema,
79
+ config: sha256Schema,
80
+ index: sha256Schema,
81
+ })
82
+ .strict(),
83
+ })
84
+ .strict();
85
+
86
+ const evidenceRefBaseSchema = z
87
+ .object({
88
+ docid: z.string().min(1).max(256).optional(),
89
+ sourceHash: sha256Schema.optional(),
90
+ mirrorHash: sha256Schema.optional(),
91
+ uri: gnoUriSchema.optional(),
92
+ seq: z.number().int().nonnegative().optional(),
93
+ startLine: z.number().int().positive().optional(),
94
+ endLine: z.number().int().positive().optional(),
95
+ score: z.number().finite().optional(),
96
+ rank: z.number().int().positive().optional(),
97
+ plannerRank: z.number().int().positive().optional(),
98
+ passageHash: sha256Schema.optional(),
99
+ sources: z.array(z.string().min(1).max(128)).max(16).optional(),
100
+ graphExpanded: z.boolean().optional(),
101
+ })
102
+ .strict();
103
+ const refineEvidenceRef = (
104
+ value: z.infer<typeof evidenceRefBaseSchema>,
105
+ context: z.RefinementCtx
106
+ ): void => {
107
+ if (!(value.docid || value.sourceHash || value.mirrorHash || value.uri)) {
108
+ context.addIssue({
109
+ code: "custom",
110
+ message: "Evidence references require a stable document identity",
111
+ });
112
+ }
113
+ if ((value.startLine === undefined) !== (value.endLine === undefined)) {
114
+ context.addIssue({
115
+ code: "custom",
116
+ message: "Evidence line ranges require both startLine and endLine",
117
+ });
118
+ }
119
+ if (
120
+ value.startLine !== undefined &&
121
+ value.endLine !== undefined &&
122
+ value.startLine > value.endLine
123
+ ) {
124
+ context.addIssue({
125
+ code: "custom",
126
+ path: ["endLine"],
127
+ message: "Evidence endLine must not precede startLine",
128
+ });
129
+ }
130
+ };
131
+ const evidenceRefSchema = evidenceRefBaseSchema.superRefine(refineEvidenceRef);
132
+ const evidencePayloadSchema = z
133
+ .object({
134
+ evidence: z.array(evidenceRefSchema).max(10_000),
135
+ latencyMs: z.number().finite().nonnegative().optional(),
136
+ })
137
+ .strict();
138
+ const retrievalPayloadSchema = z
139
+ .object({
140
+ ranked: z.array(evidenceRefSchema).max(10_000),
141
+ latencyMs: z.number().finite().nonnegative().optional(),
142
+ capabilities: z.array(z.string().min(1).max(128)).max(100).optional(),
143
+ fallbackCodes: z.array(z.string().min(1).max(128)).max(100).optional(),
144
+ })
145
+ .strict();
146
+ const contextPayloadSchema = evidencePayloadSchema.extend({
147
+ capsuleId: z.string().min(1).max(256),
148
+ });
149
+ const runPayloadSchemas = {
150
+ retrieval: retrievalPayloadSchema,
151
+ context: contextPayloadSchema,
152
+ get: evidencePayloadSchema,
153
+ } as const;
154
+ const eventPayloadSchemas = {
155
+ query: z.object({ filterFingerprint: sha256Schema.optional() }).strict(),
156
+ retrieval: retrievalPayloadSchema,
157
+ context: contextPayloadSchema,
158
+ get: evidencePayloadSchema,
159
+ open: evidencePayloadSchema,
160
+ cite: evidencePayloadSchema,
161
+ pin: evidencePayloadSchema,
162
+ capability: z
163
+ .object({
164
+ capability: z.string().min(1).max(128),
165
+ status: z.enum(["attempted", "used", "unavailable", "failed"]),
166
+ reasonCode: z.string().min(1).max(128).optional(),
167
+ })
168
+ .strict(),
169
+ complete: z
170
+ .object({
171
+ outcome: z.enum(["completed", "partial", "failed", "cancelled"]),
172
+ latencyMs: z.number().finite().nonnegative().optional(),
173
+ })
174
+ .strict(),
175
+ } as const;
176
+ const judgmentTargetSchema = evidenceRefBaseSchema
177
+ .omit({
178
+ score: true,
179
+ rank: true,
180
+ })
181
+ .superRefine(refineEvidenceRef);
182
+
183
+ export interface StartRetrievalTraceInput {
184
+ traceId?: string;
185
+ query: string;
186
+ goal?: string;
187
+ filters?: z.input<typeof traceFiltersSchema>;
188
+ fingerprints: RetrievalTraceFingerprints;
189
+ }
190
+
191
+ export type RetrievalTraceWriteResult =
192
+ | {
193
+ recorded: false;
194
+ traceId: null;
195
+ replayCapable: false;
196
+ result: "disabled";
197
+ }
198
+ | {
199
+ recorded: true;
200
+ traceId: string;
201
+ replayCapable: boolean;
202
+ result: RetrievalTraceAppendResult;
203
+ };
204
+
205
+ interface RetrievalTraceRecorderDeps {
206
+ clock?: () => number;
207
+ idFactory?: () => string;
208
+ /** Stable local secret loaded by the caller; required for metadata labels. */
209
+ redactionSecret?: string;
210
+ }
211
+
212
+ /** Query + retrieval run + retrieval event + terminal event. */
213
+ export const MIN_RETRIEVAL_TRACE_RECORDS = 4;
214
+
215
+ const normalizeText = (value: string): string =>
216
+ value.replace(/\r\n/g, "\n").replace(/\r/g, "\n").normalize("NFC");
217
+
218
+ const textShape = (value: string | undefined) => {
219
+ const normalized = normalizeText(value ?? "");
220
+ return {
221
+ characters: Array.from(normalized).length,
222
+ terms: normalized.trim() ? normalized.trim().split(/\s+/u).length : 0,
223
+ };
224
+ };
225
+
226
+ const sha256 = (value: string): string =>
227
+ new Bun.CryptoHasher("sha256").update(value).digest("hex");
228
+
229
+ const valueShape = (value: unknown): unknown => {
230
+ if (value === null) return { type: "null" };
231
+ if (Array.isArray(value)) {
232
+ const itemTypes = [
233
+ ...new Set(
234
+ value.map((item) =>
235
+ item === null ? "null" : Array.isArray(item) ? "array" : typeof item
236
+ )
237
+ ),
238
+ ].sort();
239
+ return { type: "array", count: value.length, itemTypes };
240
+ }
241
+ if (typeof value === "object") {
242
+ const fields: Record<string, unknown> = {};
243
+ for (const key of Object.keys(value as Record<string, unknown>).sort()) {
244
+ fields[key] = valueShape((value as Record<string, unknown>)[key]);
245
+ }
246
+ return { type: "object", fields };
247
+ }
248
+ return { type: typeof value };
249
+ };
250
+
251
+ const projectMetadataObject = (
252
+ value: Record<string, unknown>
253
+ ): Record<string, unknown> => ({ shape: valueShape(value) });
254
+
255
+ const projectPayload = (
256
+ _mode: RetrievalTraceRedactionMode,
257
+ value: Record<string, unknown>
258
+ ): Record<string, unknown> => value;
259
+
260
+ export class RetrievalTraceRecorder {
261
+ private readonly clock: () => number;
262
+ private readonly idFactory: () => string;
263
+ private readonly redactionSecret: string | undefined;
264
+
265
+ constructor(
266
+ private readonly store: StorePort,
267
+ private readonly config: RetrievalTraceConfig | undefined,
268
+ deps: RetrievalTraceRecorderDeps = {}
269
+ ) {
270
+ this.clock = deps.clock ?? Date.now;
271
+ this.idFactory = deps.idFactory ?? (() => crypto.randomUUID());
272
+ this.redactionSecret = deps.redactionSecret;
273
+ }
274
+
275
+ isEnabled(): boolean {
276
+ return this.config?.enabled === true;
277
+ }
278
+
279
+ async start(
280
+ input: StartRetrievalTraceInput
281
+ ): Promise<StoreResult<RetrievalTraceWriteResult>> {
282
+ if (
283
+ !this.config?.enabled ||
284
+ this.config.retention.maxRecordsPerTrace < MIN_RETRIEVAL_TRACE_RECORDS
285
+ ) {
286
+ return ok({
287
+ recorded: false,
288
+ traceId: null,
289
+ replayCapable: false,
290
+ result: "disabled",
291
+ });
292
+ }
293
+ const parsed = startTraceSchema.safeParse(input);
294
+ if (!parsed.success) {
295
+ return err("INVALID_INPUT", parsed.error.message, parsed.error);
296
+ }
297
+ const nowMs = this.clock();
298
+ if (!Number.isSafeInteger(nowMs) || nowMs < 0) {
299
+ return err("INVALID_INPUT", "Trace clock must return epoch milliseconds");
300
+ }
301
+ const traceId = parsed.data.traceId ?? this.idFactory();
302
+ const query = normalizeText(parsed.data.query);
303
+ const goal =
304
+ parsed.data.goal === undefined
305
+ ? undefined
306
+ : normalizeText(parsed.data.goal);
307
+ const filters = canonicalizeRetrievalTraceFilters(parsed.data.filters);
308
+ const replay = this.config.redactionMode === "replay";
309
+ const create = await this.store.createRetrievalTrace({
310
+ traceId,
311
+ schemaVersion: "1.0",
312
+ redactionMode: this.config.redactionMode,
313
+ replayCapable: replay,
314
+ queryText: replay ? query : null,
315
+ queryDigest: replay ? sha256(query) : null,
316
+ queryShape: textShape(query),
317
+ goalText: replay ? (goal ?? null) : null,
318
+ goalDigest: replay && goal !== undefined ? sha256(goal) : null,
319
+ goalShape: textShape(goal),
320
+ filters: replay ? filters : projectMetadataObject(filters),
321
+ fingerprints: parsed.data.fingerprints,
322
+ status: "open",
323
+ createdAtMs: nowMs,
324
+ updatedAtMs: nowMs,
325
+ expiresAtMs: nowMs + this.config.retention.maxAgeDays * 86_400_000,
326
+ });
327
+ if (!create.ok) return create;
328
+ const retention = await this.store.enforceRetrievalTraceRetention(
329
+ this.config.retention,
330
+ nowMs
331
+ );
332
+ if (!retention.ok) return retention;
333
+ const retained = await this.store.getRetrievalTrace(traceId);
334
+ if (!retained.ok) return retained;
335
+ if (!retained.value) {
336
+ return err(
337
+ "CONSTRAINT_VIOLATION",
338
+ `Retrieval trace ${traceId} exceeded retention limits and was evicted`
339
+ );
340
+ }
341
+ return ok({
342
+ recorded: true,
343
+ traceId,
344
+ replayCapable: replay,
345
+ result: create.value,
346
+ });
347
+ }
348
+
349
+ async appendRun(
350
+ input: RetrievalTraceRunInput
351
+ ): Promise<StoreResult<RetrievalTraceAppendResult | "disabled">> {
352
+ if (!this.config?.enabled) return ok("disabled");
353
+ const payload = runPayloadSchemas[input.kind]?.safeParse(input.payload);
354
+ if (!payload?.success) {
355
+ return err(
356
+ "INVALID_INPUT",
357
+ payload?.error.message ?? `Unknown retrieval run kind: ${input.kind}`
358
+ );
359
+ }
360
+ try {
361
+ const parsed = parseRetrievalTraceRunInput({
362
+ ...input,
363
+ payload: projectPayload(this.config.redactionMode, payload.data),
364
+ });
365
+ return await this.enforceAfterWrite(
366
+ input.traceId,
367
+ await this.store.appendRetrievalTraceRun(parsed)
368
+ );
369
+ } catch (cause) {
370
+ return invalidTraceInput(cause, "Invalid retrieval trace run");
371
+ }
372
+ }
373
+
374
+ async appendEvent(
375
+ input: RetrievalTraceEventInput
376
+ ): Promise<StoreResult<RetrievalTraceAppendResult | "disabled">> {
377
+ if (!this.config?.enabled) return ok("disabled");
378
+ const payload = eventPayloadSchemas[input.kind]?.safeParse(input.payload);
379
+ if (!payload?.success) {
380
+ return err(
381
+ "INVALID_INPUT",
382
+ payload?.error.message ?? `Unknown retrieval event kind: ${input.kind}`
383
+ );
384
+ }
385
+ try {
386
+ const parsed = parseRetrievalTraceEventInput({
387
+ ...input,
388
+ payload: projectPayload(this.config.redactionMode, payload.data),
389
+ });
390
+ return await this.enforceAfterWrite(
391
+ input.traceId,
392
+ await this.store.appendRetrievalTraceEvent(parsed)
393
+ );
394
+ } catch (cause) {
395
+ return invalidTraceInput(cause, "Invalid retrieval trace event");
396
+ }
397
+ }
398
+
399
+ async appendJudgment(
400
+ input: RetrievalTraceJudgmentInput
401
+ ): Promise<StoreResult<RetrievalTraceAppendResult | "disabled">> {
402
+ if (!this.config?.enabled) return ok("disabled");
403
+ const target = judgmentTargetSchema.safeParse(input.target);
404
+ if (!target.success) {
405
+ return err("INVALID_INPUT", target.error.message, target.error);
406
+ }
407
+ const metadata = this.config.redactionMode === "metadata";
408
+ if (metadata && !this.redactionSecret) {
409
+ return err(
410
+ "INVALID_INPUT",
411
+ "Metadata judgments require a stable local redaction secret"
412
+ );
413
+ }
414
+ try {
415
+ const parsed = parseRetrievalTraceJudgmentInput({
416
+ ...input,
417
+ targetRef: metadata
418
+ ? `redacted:${sha256(`${this.redactionSecret}\0${input.targetRef}`)}`
419
+ : input.targetRef,
420
+ target: projectPayload(this.config.redactionMode, target.data),
421
+ });
422
+ return await this.enforceAfterWrite(
423
+ input.traceId,
424
+ await this.store.appendRetrievalTraceJudgment(parsed)
425
+ );
426
+ } catch (cause) {
427
+ return invalidTraceInput(cause, "Invalid retrieval trace judgment");
428
+ }
429
+ }
430
+
431
+ async finalize(
432
+ traceId: string,
433
+ status: RetrievalTraceTerminalStatus
434
+ ): Promise<StoreResult<RetrievalTraceAppendResult | "disabled">> {
435
+ if (!this.config?.enabled) return ok("disabled");
436
+ return await this.enforceAfterWrite(
437
+ traceId,
438
+ await this.store.finalizeRetrievalTrace(traceId, status, this.clock())
439
+ );
440
+ }
441
+
442
+ private async enforceAfterWrite(
443
+ traceId: string,
444
+ result: StoreResult<RetrievalTraceAppendResult>
445
+ ): Promise<StoreResult<RetrievalTraceAppendResult>> {
446
+ if (!result.ok || !this.config?.enabled) return result;
447
+ const retained = await this.store.enforceRetrievalTraceRetention(
448
+ this.config.retention,
449
+ this.clock()
450
+ );
451
+ if (!retained.ok) return retained;
452
+ const stored = await this.store.getRetrievalTrace(traceId);
453
+ if (!stored.ok) return stored;
454
+ if (!stored.value) {
455
+ return err(
456
+ "CONSTRAINT_VIOLATION",
457
+ `Retrieval trace ${traceId} exceeded retention limits and was evicted`
458
+ );
459
+ }
460
+ return result;
461
+ }
462
+ }
463
+
464
+ const invalidTraceInput = <T>(
465
+ cause: unknown,
466
+ fallback: string
467
+ ): StoreResult<T> =>
468
+ err(
469
+ "INVALID_INPUT",
470
+ cause instanceof Error ? cause.message : fallback,
471
+ cause
472
+ );
package/src/llm/errors.ts CHANGED
@@ -22,7 +22,8 @@ export type LlmErrorCode =
22
22
  | "OUT_OF_MEMORY"
23
23
  | "INVALID_URI"
24
24
  | "LOCK_FAILED"
25
- | "AUTO_DOWNLOAD_DISABLED";
25
+ | "AUTO_DOWNLOAD_DISABLED"
26
+ | "STRUCTURED_OUTPUT_UNAVAILABLE";
26
27
 
27
28
  export interface LlmError {
28
29
  code: LlmErrorCode;
@@ -248,3 +249,11 @@ export function autoDownloadDisabledError(uri: string): LlmError {
248
249
  suggestion: "Run 'gno models pull' to download models manually.",
249
250
  });
250
251
  }
252
+
253
+ export function structuredOutputUnavailableError(uri: string): LlmError {
254
+ return llmError("STRUCTURED_OUTPUT_UNAVAILABLE", {
255
+ message: `JSON Schema constrained generation is unavailable for model: ${uri}`,
256
+ modelUri: uri,
257
+ retryable: false,
258
+ });
259
+ }
@@ -7,7 +7,10 @@
7
7
 
8
8
  import type { GenerationPort, GenParams, LlmResult } from "./types";
9
9
 
10
- import { inferenceFailedError } from "./errors";
10
+ import {
11
+ inferenceFailedError,
12
+ structuredOutputUnavailableError,
13
+ } from "./errors";
11
14
 
12
15
  // ─────────────────────────────────────────────────────────────────────────────
13
16
  // Types
@@ -42,6 +45,7 @@ export class HttpGeneration implements GenerationPort {
42
45
  private readonly apiUrl: string;
43
46
  private readonly modelName: string;
44
47
  readonly modelUri: string;
48
+ readonly structuredOutput = "none" as const;
45
49
 
46
50
  constructor(modelUri: string) {
47
51
  this.modelUri = modelUri;
@@ -63,6 +67,12 @@ export class HttpGeneration implements GenerationPort {
63
67
  prompt: string,
64
68
  params?: GenParams
65
69
  ): Promise<LlmResult<string>> {
70
+ if (params?.jsonSchema) {
71
+ return {
72
+ ok: false,
73
+ error: structuredOutputUnavailableError(this.modelUri),
74
+ };
75
+ }
66
76
  try {
67
77
  const response = await fetch(this.apiUrl, {
68
78
  method: "POST",
@@ -18,6 +18,39 @@ type LlamaModel = Awaited<
18
18
  Awaited<ReturnType<typeof import("node-llama-cpp").getLlama>>["loadModel"]
19
19
  >
20
20
  >;
21
+ type Llama = Awaited<ReturnType<typeof import("node-llama-cpp").getLlama>>;
22
+ type JsonGrammarSchema = Parameters<Llama["createGrammarForJsonSchema"]>[0];
23
+
24
+ export interface JsonSchemaGrammarLike {
25
+ parse(response: string): unknown;
26
+ }
27
+
28
+ export interface StructuredPromptSession {
29
+ prompt(
30
+ prompt: string,
31
+ options: {
32
+ temperature: number;
33
+ seed: number;
34
+ maxTokens: number;
35
+ grammar?: JsonSchemaGrammarLike;
36
+ }
37
+ ): Promise<string>;
38
+ }
39
+
40
+ export const promptWithJsonSchemaGrammar = async (
41
+ session: StructuredPromptSession,
42
+ prompt: string,
43
+ options: {
44
+ temperature: number;
45
+ seed: number;
46
+ maxTokens: number;
47
+ },
48
+ grammar?: JsonSchemaGrammarLike
49
+ ): Promise<string> => {
50
+ const response = await session.prompt(prompt, { ...options, grammar });
51
+ grammar?.parse(response);
52
+ return response;
53
+ };
21
54
 
22
55
  // ─────────────────────────────────────────────────────────────────────────────
23
56
  // Default Parameters (for determinism)
@@ -34,6 +67,7 @@ const DEFAULT_MAX_TOKENS = 256;
34
67
  export class NodeLlamaCppGeneration implements GenerationPort {
35
68
  private readonly manager: ModelManager;
36
69
  readonly modelUri: string;
70
+ readonly structuredOutput = "json_schema" as const;
37
71
  private readonly modelPath: string;
38
72
 
39
73
  constructor(manager: ModelManager, modelUri: string, modelPath: string) {
@@ -56,11 +90,16 @@ export class NodeLlamaCppGeneration implements GenerationPort {
56
90
  }
57
91
 
58
92
  const llamaModel = model.value.model as LlamaModel;
59
- const context = await llamaModel.createContext(
60
- params?.contextSize ? { contextSize: params.contextSize } : undefined
61
- );
62
-
93
+ let context: Awaited<ReturnType<LlamaModel["createContext"]>> | null = null;
63
94
  try {
95
+ const grammar = params?.jsonSchema
96
+ ? await (
97
+ await this.manager.getLlama()
98
+ ).createGrammarForJsonSchema(params.jsonSchema as JsonGrammarSchema)
99
+ : undefined;
100
+ context = await llamaModel.createContext(
101
+ params?.contextSize ? { contextSize: params.contextSize } : undefined
102
+ );
64
103
  // Import LlamaChatSession dynamically
65
104
  const { LlamaChatSession } = await import("node-llama-cpp");
66
105
  const session = new LlamaChatSession({
@@ -68,17 +107,22 @@ export class NodeLlamaCppGeneration implements GenerationPort {
68
107
  });
69
108
 
70
109
  // Note: stop sequences not yet supported - requires stopOnTrigger API
71
- const response = await session.prompt(prompt, {
72
- temperature: params?.temperature ?? DEFAULT_TEMPERATURE,
73
- seed: params?.seed ?? DEFAULT_SEED,
74
- maxTokens: params?.maxTokens ?? DEFAULT_MAX_TOKENS,
75
- });
110
+ const response = await promptWithJsonSchemaGrammar(
111
+ session as StructuredPromptSession,
112
+ prompt,
113
+ {
114
+ temperature: params?.temperature ?? DEFAULT_TEMPERATURE,
115
+ seed: params?.seed ?? DEFAULT_SEED,
116
+ maxTokens: params?.maxTokens ?? DEFAULT_MAX_TOKENS,
117
+ },
118
+ grammar
119
+ );
76
120
 
77
121
  return { ok: true, value: response };
78
122
  } catch (e) {
79
123
  return { ok: false, error: inferenceFailedError(this.modelUri, e) };
80
124
  } finally {
81
- await context.dispose().catch(() => {
125
+ await context?.dispose().catch(() => {
82
126
  // Ignore disposal errors
83
127
  });
84
128
  }
package/src/llm/types.ts CHANGED
@@ -58,8 +58,12 @@ export interface GenParams {
58
58
  contextSize?: number;
59
59
  /** Stop sequences */
60
60
  stop?: string[];
61
+ /** Closed JSON Schema enforced by a capable generation backend. */
62
+ jsonSchema?: Readonly<Record<string, unknown>>;
61
63
  }
62
64
 
65
+ export type StructuredOutputCapability = "json_schema" | "none";
66
+
63
67
  // ─────────────────────────────────────────────────────────────────────────────
64
68
  // Rerank Types
65
69
  // ─────────────────────────────────────────────────────────────────────────────
@@ -90,6 +94,8 @@ export interface EmbeddingPort {
90
94
 
91
95
  export interface GenerationPort {
92
96
  readonly modelUri: string;
97
+ /** Undefined is treated as unsupported for backwards-compatible ports. */
98
+ readonly structuredOutput?: StructuredOutputCapability;
93
99
  generate(prompt: string, params?: GenParams): Promise<LlmResult<string>>;
94
100
  dispose(): Promise<void>;
95
101
  }