@gmickel/gno 1.19.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 (45) hide show
  1. package/README.md +12 -7
  2. package/assets/skill/SKILL.md +27 -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 +1 -1
  6. package/spec/cli.md +42 -17
  7. package/spec/evals-agentic.md +87 -5
  8. package/spec/mcp.md +53 -3
  9. package/spec/output-schemas/ask.schema.json +198 -0
  10. package/spec/output-schemas/claim-verification.schema.json +291 -0
  11. package/spec/output-schemas/context-capsule-v1.schema.json +36 -1
  12. package/src/app/context-runtime-contract.ts +10 -5
  13. package/src/app/context-runtime-input.ts +29 -1
  14. package/src/app/context-runtime-types.ts +4 -0
  15. package/src/app/context-runtime.ts +5 -1
  16. package/src/app/context-surface.ts +4 -0
  17. package/src/app/verified-ask.ts +291 -0
  18. package/src/cli/commands/ask-format.ts +255 -0
  19. package/src/cli/commands/ask.ts +40 -149
  20. package/src/cli/program.ts +32 -1
  21. package/src/core/context-budget.ts +6 -0
  22. package/src/core/context-capsule-retrieval-schema.ts +4 -0
  23. package/src/core/context-capsule-schema.ts +17 -0
  24. package/src/core/context-capsule-validation.ts +3 -2
  25. package/src/core/context-capsule.ts +18 -0
  26. package/src/core/context-compiler.ts +33 -21
  27. package/src/core/context-evidence.ts +6 -0
  28. package/src/core/retrieval-trace-evidence-origin.ts +3 -0
  29. package/src/core/retrieval-trace-session.ts +15 -2
  30. package/src/llm/errors.ts +10 -1
  31. package/src/llm/httpGeneration.ts +11 -1
  32. package/src/llm/nodeLlamaCpp/generation.ts +54 -10
  33. package/src/llm/types.ts +6 -0
  34. package/src/mcp/tools/ask.ts +228 -0
  35. package/src/mcp/tools/context.ts +28 -7
  36. package/src/mcp/tools/index.ts +9 -0
  37. package/src/pipeline/claim-verification-schema.ts +235 -0
  38. package/src/pipeline/claim-verification.ts +487 -0
  39. package/src/pipeline/claim-verifier.ts +474 -0
  40. package/src/pipeline/types.ts +25 -0
  41. package/src/sdk/client.ts +35 -2
  42. package/src/serve/public/components/AskVerificationPanel.tsx +189 -0
  43. package/src/serve/public/globals.built.css +1 -1
  44. package/src/serve/public/pages/Ask.tsx +42 -4
  45. package/src/serve/routes/api.ts +149 -3
@@ -14,6 +14,7 @@ import type {
14
14
  } from "../../llm/types";
15
15
  import type { AskOptions, AskResult, Citation } from "../../pipeline/types";
16
16
 
17
+ import { buildVerifiedAsk } from "../../app/verified-ask";
17
18
  import {
18
19
  finishRetrievalTraceAfterError,
19
20
  retrievalTraceFilters,
@@ -39,6 +40,8 @@ import {
39
40
  } from "../progress";
40
41
  import { initStore } from "./shared";
41
42
 
43
+ export { formatAsk } from "./ask-format";
44
+
42
45
  // ─────────────────────────────────────────────────────────────────────────────
43
46
  // Types
44
47
  // ─────────────────────────────────────────────────────────────────────────────
@@ -83,9 +86,11 @@ export async function ask(
83
86
  options: AskCommandOptions = {}
84
87
  ): Promise<AskCommandResult> {
85
88
  const limit = options.limit ?? 5;
89
+ const globals = getGlobals();
86
90
 
87
91
  const initResult = await initStore({
88
92
  configPath: options.configPath,
93
+ indexName: globals.index,
89
94
  collection: options.collection,
90
95
  syncConfig: false,
91
96
  });
@@ -103,7 +108,9 @@ export async function ask(
103
108
  let traceSession: RetrievalTraceSession | undefined;
104
109
 
105
110
  try {
106
- const answerRequested = Boolean(options.answer && !options.noAnswer);
111
+ const verificationRequested = options.verify === true;
112
+ const answerRequested =
113
+ verificationRequested || Boolean(options.answer && !options.noAnswer);
107
114
  const embedUri = resolveModelUri(
108
115
  config,
109
116
  "embed",
@@ -111,7 +118,7 @@ export async function ask(
111
118
  options.collection
112
119
  );
113
120
  const expandUri =
114
- !options.noExpand && !options.queryModes?.length
121
+ !verificationRequested && !options.noExpand && !options.queryModes?.length
115
122
  ? resolveModelUri(
116
123
  config,
117
124
  "expand",
@@ -136,6 +143,7 @@ export async function ask(
136
143
  query,
137
144
  filters: retrievalTraceFilters({ ...options, limit }),
138
145
  pipeline: "ask",
146
+ indexName: globals.index,
139
147
  modelUris: [embedUri, expandUri, answerUri, rerankUri].filter(
140
148
  (value): value is string => Boolean(value)
141
149
  ),
@@ -147,7 +155,6 @@ export async function ask(
147
155
  const llm = new LlmAdapter(config);
148
156
 
149
157
  // Resolve download policy from env/flags
150
- const globals = getGlobals();
151
158
  const policy = resolveDownloadPolicy(process.env, {
152
159
  offline: globals.offline,
153
160
  });
@@ -254,6 +261,34 @@ export async function ask(
254
261
  };
255
262
  }
256
263
 
264
+ if (verificationRequested && answerPort) {
265
+ const verified = await buildVerifiedAsk(
266
+ query,
267
+ { ...options, limit },
268
+ {
269
+ store,
270
+ config,
271
+ indexName: globals.index,
272
+ vectorIndex,
273
+ embedPort,
274
+ rerankPort,
275
+ genPort: answerPort,
276
+ traceSession,
277
+ }
278
+ );
279
+ const finalized = await traceSession?.finish(
280
+ answerTraceTerminalStatus(verified.citations)
281
+ );
282
+ if (finalized && !finalized.ok) {
283
+ return { success: false, error: finalized.error.message };
284
+ }
285
+ return {
286
+ success: true,
287
+ data: verified,
288
+ metadata: traceSession?.metadata(),
289
+ };
290
+ }
291
+
257
292
  // Run hybrid search
258
293
  const searchResult = await searchHybrid(deps, query, {
259
294
  limit,
@@ -267,6 +302,8 @@ export async function ask(
267
302
  tagsAll: options.tagsAll,
268
303
  tagsAny: options.tagsAny,
269
304
  exclude: options.exclude,
305
+ minScore: options.minScore,
306
+ graph: options.graph,
270
307
  queryModes: options.queryModes,
271
308
  noExpand: options.noExpand,
272
309
  noRerank: options.noRerank,
@@ -394,149 +431,3 @@ export async function ask(
394
431
  await store.close();
395
432
  }
396
433
  }
397
-
398
- // ─────────────────────────────────────────────────────────────────────────────
399
- // Formatters
400
- // ─────────────────────────────────────────────────────────────────────────────
401
-
402
- interface FormatOptions {
403
- showSources?: boolean;
404
- }
405
-
406
- // oxlint-disable-next-line max-lines-per-function -- terminal formatting with conditional sections
407
- function formatTerminal(data: AskResult, opts: FormatOptions = {}): string {
408
- const lines: string[] = [];
409
- const hasAnswer = Boolean(data.answer);
410
-
411
- // Show answer if present
412
- if (data.answer) {
413
- lines.push("Answer:");
414
- lines.push(data.answer);
415
- lines.push("");
416
- }
417
-
418
- // Show cited sources (only sources actually referenced in answer)
419
- if (data.citations && data.citations.length > 0) {
420
- lines.push("Cited Sources:");
421
- for (let i = 0; i < data.citations.length; i++) {
422
- const c = data.citations[i];
423
- if (c) {
424
- lines.push(` [${i + 1}] ${c.uri}`);
425
- }
426
- }
427
- lines.push("");
428
- }
429
-
430
- // Show all retrieved sources if:
431
- // - No answer was generated (retrieval-only mode)
432
- // - User explicitly requested with --show-sources
433
- const showAllSources = !hasAnswer || opts.showSources;
434
- if (showAllSources && data.results.length > 0) {
435
- lines.push(hasAnswer ? "All Retrieved Sources:" : "Sources:");
436
- for (const r of data.results) {
437
- lines.push(` [${r.docid}] ${r.uri}`);
438
- if (r.title) {
439
- lines.push(` ${r.title}`);
440
- }
441
- }
442
- } else if (hasAnswer && data.results.length > 0) {
443
- // Hint about --show-sources when we have more sources
444
- const citedCount = data.citations?.length ?? 0;
445
- if (data.results.length > citedCount) {
446
- lines.push(
447
- `(${data.results.length} sources retrieved, use --show-sources to list all)`
448
- );
449
- }
450
- }
451
-
452
- if (!data.answer && data.results.length === 0) {
453
- lines.push("No relevant sources found.");
454
- }
455
-
456
- return lines.join("\n");
457
- }
458
-
459
- function formatMarkdown(data: AskResult, opts: FormatOptions = {}): string {
460
- const lines: string[] = [];
461
- const hasAnswer = Boolean(data.answer);
462
-
463
- lines.push(`# Question: ${data.query}`);
464
- lines.push("");
465
-
466
- if (data.answer) {
467
- lines.push("## Answer");
468
- lines.push("");
469
- lines.push(data.answer);
470
- lines.push("");
471
- }
472
-
473
- // Show cited sources (only sources actually referenced in answer)
474
- if (data.citations && data.citations.length > 0) {
475
- lines.push("## Cited Sources");
476
- lines.push("");
477
- for (let i = 0; i < data.citations.length; i++) {
478
- const c = data.citations[i];
479
- if (c) {
480
- lines.push(`**[${i + 1}]** \`${c.uri}\``);
481
- }
482
- }
483
- lines.push("");
484
- }
485
-
486
- // Show all retrieved sources if no answer or --show-sources
487
- const showAllSources = !hasAnswer || opts.showSources;
488
- if (showAllSources) {
489
- lines.push(hasAnswer ? "## All Retrieved Sources" : "## Sources");
490
- lines.push("");
491
-
492
- for (let i = 0; i < data.results.length; i++) {
493
- const r = data.results[i];
494
- if (!r) {
495
- continue;
496
- }
497
- lines.push(`${i + 1}. **${r.title || r.source.relPath}**`);
498
- lines.push(` - URI: \`${r.uri}\``);
499
- lines.push(` - Score: ${r.score.toFixed(2)}`);
500
- }
501
-
502
- if (data.results.length === 0) {
503
- lines.push("*No relevant sources found.*");
504
- }
505
- }
506
-
507
- lines.push("");
508
- lines.push("---");
509
- lines.push(
510
- `*Mode: ${data.mode} | Expanded: ${data.meta.expanded} | Reranked: ${data.meta.reranked}*`
511
- );
512
-
513
- return lines.join("\n");
514
- }
515
-
516
- /**
517
- * Format ask result for output.
518
- */
519
- export function formatAsk(
520
- result: AskCommandResult,
521
- options: AskCommandOptions
522
- ): string {
523
- if (!result.success) {
524
- return options.json
525
- ? JSON.stringify({
526
- error: { code: "ASK_FAILED", message: result.error },
527
- })
528
- : `Error: ${result.error}`;
529
- }
530
-
531
- const formatOpts: FormatOptions = { showSources: options.showSources };
532
-
533
- if (options.json) {
534
- return JSON.stringify(result.data, null, 2);
535
- }
536
-
537
- if (options.md) {
538
- return formatMarkdown(result.data, formatOpts);
539
- }
540
-
541
- return formatTerminal(result.data, formatOpts);
542
- }
@@ -635,7 +635,9 @@ function wireSearchCommands(program: Command): void {
635
635
 
636
636
  const limit = cmdOpts.limit
637
637
  ? parsePositiveInt("limit", cmdOpts.limit)
638
- : getDefaultLimit(format);
638
+ : cmdOpts.verify
639
+ ? 5
640
+ : getDefaultLimit(format);
639
641
  const categories = parseCsvValues(cmdOpts.category);
640
642
  const exclude = parseCsvValues(cmdOpts.exclude);
641
643
 
@@ -1108,8 +1110,16 @@ function wireSearchCommands(program: Command): void {
1108
1110
  )
1109
1111
  .option("-C, --candidate-limit <num>", "max candidates passed to reranking")
1110
1112
  .option("--answer", "generate short grounded answer")
1113
+ .option(
1114
+ "--verify",
1115
+ "generate and verify every claim against a closed Context Capsule"
1116
+ )
1111
1117
  .option("--no-answer", "force retrieval-only output")
1112
1118
  .option("--max-answer-tokens <num>", "max answer tokens")
1119
+ .option("--context-budget-tokens <num>", "verified Context token budget")
1120
+ .option("--context-budget-bytes <num>", "verified Context byte budget")
1121
+ .option("--min-score <score>", "minimum retrieval score (0-1)")
1122
+ .option("--graph", "include bounded graph expansion")
1113
1123
  .option("--show-sources", "show all retrieved sources (not just cited)")
1114
1124
  .option("--json", "JSON output")
1115
1125
  .option("--md", "Markdown output")
@@ -1140,6 +1150,22 @@ function wireSearchCommands(program: Command): void {
1140
1150
  const maxAnswerTokens = cmdOpts.maxAnswerTokens
1141
1151
  ? parsePositiveInt("max-answer-tokens", cmdOpts.maxAnswerTokens)
1142
1152
  : undefined;
1153
+ const contextBudgetTokens = cmdOpts.contextBudgetTokens
1154
+ ? parsePositiveInt("context-budget-tokens", cmdOpts.contextBudgetTokens)
1155
+ : undefined;
1156
+ const contextBudgetBytes = cmdOpts.contextBudgetBytes
1157
+ ? parsePositiveInt("context-budget-bytes", cmdOpts.contextBudgetBytes)
1158
+ : undefined;
1159
+ const minScore = parseOptionalFloat("min-score", cmdOpts.minScore);
1160
+ if (minScore !== undefined && (minScore < 0 || minScore > 1)) {
1161
+ throw new CliError("VALIDATION", "min-score must be between 0 and 1");
1162
+ }
1163
+ if (cmdOpts.verify && cmdOpts.noAnswer) {
1164
+ throw new CliError(
1165
+ "VALIDATION",
1166
+ "--verify cannot be combined with --no-answer"
1167
+ );
1168
+ }
1143
1169
  const categories = parseCsvValues(cmdOpts.category);
1144
1170
  const exclude = parseCsvValues(cmdOpts.exclude);
1145
1171
 
@@ -1188,6 +1214,8 @@ function wireSearchCommands(program: Command): void {
1188
1214
  author: cmdOpts.author as string | undefined,
1189
1215
  intent: cmdOpts.intent as string | undefined,
1190
1216
  exclude,
1217
+ minScore,
1218
+ graph: Boolean(cmdOpts.graph),
1191
1219
  queryModes,
1192
1220
  noExpand: depthPolicy.noExpand,
1193
1221
  noRerank: depthPolicy.noRerank,
@@ -1196,7 +1224,10 @@ function wireSearchCommands(program: Command): void {
1196
1224
  // Commander creates separate cmdOpts.noAnswer for --no-answer flag
1197
1225
  answer: Boolean(cmdOpts.answer),
1198
1226
  noAnswer: Boolean(cmdOpts.noAnswer),
1227
+ verify: Boolean(cmdOpts.verify),
1199
1228
  maxAnswerTokens,
1229
+ contextBudgetTokens,
1230
+ contextBudgetBytes,
1200
1231
  showSources,
1201
1232
  json: format === "json",
1202
1233
  md: format === "md",
@@ -6,6 +6,8 @@
6
6
  * canonical Capsule payload, including coverage, omissions, and guidance.
7
7
  */
8
8
 
9
+ import type { FusionSource } from "../pipeline/types";
10
+
9
11
  export const CONTEXT_OMISSION_REASONS = [
10
12
  "duplicate",
11
13
  "overlap",
@@ -43,6 +45,10 @@ export interface MaterializedContextCandidate<
43
45
  text: string;
44
46
  facets: string[];
45
47
  retrievalRank: number;
48
+ /** Absent only for legacy results that predate planner provenance metadata. */
49
+ retrievalSources?: FusionSource[];
50
+ /** Absent only for legacy results that predate planner provenance metadata. */
51
+ graphExpanded?: boolean;
46
52
  value: T;
47
53
  }
48
54
 
@@ -59,10 +59,14 @@ export const contextCapsuleRetrievalSchema = z
59
59
  .object({
60
60
  author: z.string().min(1).max(256).nullable(),
61
61
  lang: z.string().min(1).max(64).nullable(),
62
+ intent: z.string().min(1).max(16_384).nullable().optional(),
63
+ exclude: z.array(nonEmptyText.max(256)).max(128).optional(),
64
+ minScore: z.number().min(0).max(1).nullable().optional(),
62
65
  queryModes: z.array(queryModeSchema).max(128),
63
66
  limit: z.number().int().positive(),
64
67
  candidateLimit: z.number().int().positive(),
65
68
  graphRequested: z.boolean(),
69
+ rerankRequested: z.boolean().optional(),
66
70
  })
67
71
  .strict(),
68
72
  capabilityStates: z
@@ -29,6 +29,14 @@ const positiveIntegerSchema = z.number().int().positive();
29
29
  const nonNegativeIntegerSchema = z.number().int().nonnegative();
30
30
  const COLLECTION_PATTERN = /^[a-z0-9][a-z0-9_-]{0,63}$/;
31
31
  const collectionSchema = nonEmptyTextSchema.max(64).regex(COLLECTION_PATTERN);
32
+ const retrievalSourceSchema = z.enum([
33
+ "bm25",
34
+ "vector",
35
+ "bm25_variant",
36
+ "vector_variant",
37
+ "hyde",
38
+ "graph",
39
+ ]);
32
40
  const compareCodeUnits = (left: string, right: string): number =>
33
41
  left < right ? -1 : left > right ? 1 : 0;
34
42
 
@@ -244,6 +252,15 @@ export const contextCapsuleEvidenceSchema = z
244
252
  contextIds: z.array(sha256Schema).max(128),
245
253
  retrievalRank: positiveIntegerSchema,
246
254
  selectionRank: positiveIntegerSchema,
255
+ retrievalSources: z
256
+ .array(retrievalSourceSchema)
257
+ .min(1)
258
+ .max(6)
259
+ .refine((sources) => new Set(sources).size === sources.length, {
260
+ message: "retrievalSources must be unique",
261
+ })
262
+ .optional(),
263
+ graphExpanded: z.boolean().optional(),
247
264
  facets: z.array(nonEmptyTextSchema.max(512)).max(128),
248
265
  trust: z.literal("untrusted"),
249
266
  egress: z.enum([
@@ -352,11 +352,12 @@ export const validateContextCapsulePayload = (
352
352
  });
353
353
  }
354
354
  const semanticRequested = value.retrieval.depthPolicy !== "fast";
355
+ const rerankRequested =
356
+ value.retrieval.request.rerankRequested ?? semanticRequested;
355
357
  if (
356
358
  value.retrieval.capabilityStates.semanticSearch.requested !==
357
359
  semanticRequested ||
358
- value.retrieval.capabilityStates.reranking.requested !==
359
- semanticRequested ||
360
+ value.retrieval.capabilityStates.reranking.requested !== rerankRequested ||
360
361
  value.retrieval.capabilityStates.graphExpansion.requested !==
361
362
  value.retrieval.request.graphRequested
362
363
  ) {
@@ -84,6 +84,17 @@ const normalizePayload = (
84
84
  value.retrieval.request.lang === null
85
85
  ? null
86
86
  : normalizeText(value.retrieval.request.lang),
87
+ ...(value.retrieval.request.intent === undefined
88
+ ? {}
89
+ : {
90
+ intent:
91
+ value.retrieval.request.intent === null
92
+ ? null
93
+ : normalizeText(value.retrieval.request.intent),
94
+ }),
95
+ ...(value.retrieval.request.exclude === undefined
96
+ ? {}
97
+ : { exclude: normalizeSet(value.retrieval.request.exclude) }),
87
98
  queryModes: value.retrieval.request.queryModes.map((mode) => ({
88
99
  ...mode,
89
100
  text: normalizeText(mode.text),
@@ -110,6 +121,13 @@ const normalizePayload = (
110
121
  documentDate: normalizeDocumentDate(item.documentDate),
111
122
  observedAt: normalizeDate(item.observedAt),
112
123
  contextIds: normalizeSet(item.contextIds),
124
+ ...(item.retrievalSources === undefined
125
+ ? {}
126
+ : {
127
+ retrievalSources: [...new Set(item.retrievalSources)].sort(
128
+ compareCodeUnits
129
+ ),
130
+ }),
113
131
  facets: normalizeSet(item.facets),
114
132
  })),
115
133
  guidance: {
@@ -64,8 +64,8 @@ export interface ContextRetrievalRequest extends HybridSearchOptions {
64
64
  export interface ContextRetrievalCandidate {
65
65
  result: SearchResult;
66
66
  retrievalRank: number;
67
- retrievalSources: FusionSource[];
68
- graphExpanded: boolean;
67
+ retrievalSources?: FusionSource[];
68
+ graphExpanded?: boolean;
69
69
  contextIds: string[];
70
70
  observedAt: string | null;
71
71
  }
@@ -97,9 +97,13 @@ export interface ContextCompilerInput {
97
97
  categories?: string[];
98
98
  author?: string;
99
99
  lang?: string;
100
+ intent?: string;
101
+ exclude?: string[];
102
+ minScore?: number;
100
103
  since?: string;
101
104
  until?: string;
102
105
  graph?: boolean;
106
+ noRerank?: boolean;
103
107
  limit?: number;
104
108
  candidateLimit?: number;
105
109
  /** Frozen once by the caller; never defaulted from wall-clock time. */
@@ -164,16 +168,9 @@ const compareCodeUnits = (left: string, right: string): number => {
164
168
  };
165
169
 
166
170
  const plannerMeta = (
167
- result: SearchResult,
168
- fallbackRank: number
169
- ): SearchResultPlannerMetadata =>
170
- result[SEARCH_RESULT_PLANNER_METADATA] ?? {
171
- retrievalRank: fallbackRank,
172
- mirrorHash: result.conversion?.mirrorHash ?? "",
173
- seq: 0,
174
- sources: [],
175
- graphExpanded: false,
176
- };
171
+ result: SearchResult
172
+ ): SearchResultPlannerMetadata | undefined =>
173
+ result[SEARCH_RESULT_PLANNER_METADATA];
177
174
 
178
175
  const compareSearchResults = (
179
176
  left: SearchResult,
@@ -219,7 +216,9 @@ const referenceFromResult = (
219
216
  const normalizeMaterialized = <T>(
220
217
  draft: ContextMaterializedDraft<T>,
221
218
  facets: string[],
222
- retrievalRank: number
219
+ retrievalRank: number,
220
+ retrievalSources: FusionSource[] | undefined,
221
+ graphExpanded: boolean | undefined
223
222
  ): MaterializedContextCandidate<T> => {
224
223
  const text = draft.text;
225
224
  if (
@@ -250,6 +249,10 @@ const normalizeMaterialized = <T>(
250
249
  text,
251
250
  facets,
252
251
  retrievalRank,
252
+ ...(retrievalSources === undefined
253
+ ? {}
254
+ : { retrievalSources: [...retrievalSources].sort(compareCodeUnits) }),
255
+ ...(graphExpanded === undefined ? {} : { graphExpanded }),
253
256
  value: draft.value,
254
257
  };
255
258
  };
@@ -352,10 +355,13 @@ export const planContextEvidence = async <T, P>(
352
355
  categories: input.categories,
353
356
  author: input.author,
354
357
  lang: input.lang,
358
+ intent: input.intent,
359
+ exclude: input.exclude,
360
+ minScore: input.minScore,
355
361
  since: temporalRange.since,
356
362
  until: temporalRange.until,
357
363
  graph: hasRerankBudget ? input.graph : false,
358
- noRerank: hasRerankBudget ? undefined : true,
364
+ noRerank: input.noRerank || !hasRerankBudget ? true : undefined,
359
365
  limit: resultLimit === undefined ? undefined : Math.max(1, resultLimit),
360
366
  candidateLimit:
361
367
  rerankLimit === undefined ? undefined : Math.max(1, rerankLimit),
@@ -377,7 +383,7 @@ export const planContextEvidence = async <T, P>(
377
383
  const results = decoratedResults
378
384
  .map((result, index) => ({
379
385
  result,
380
- retrievalRank: plannerMeta(result, index + 1).retrievalRank,
386
+ retrievalRank: plannerMeta(result)?.retrievalRank ?? index + 1,
381
387
  }))
382
388
  .sort(
383
389
  (left, right) =>
@@ -407,8 +413,8 @@ export const planContextEvidence = async <T, P>(
407
413
  const referencesByCandidate: ContextCandidateReference[] = [];
408
414
 
409
415
  for (const [index, result] of results.entries()) {
410
- const meta = plannerMeta(result, index + 1);
411
- for (const source of meta.sources) retrievalSources.add(source);
416
+ const meta = plannerMeta(result);
417
+ for (const source of meta?.sources ?? []) retrievalSources.add(source);
412
418
  const retrievalReference = referenceFromResult(result);
413
419
  if (!isContextUriInScope(result.uri, indexName, collections, uriPrefix)) {
414
420
  for (const facet of facetPlan) {
@@ -431,9 +437,13 @@ export const planContextEvidence = async <T, P>(
431
437
  }
432
438
  plannedCandidates.push({
433
439
  result,
434
- retrievalRank: meta.retrievalRank,
435
- retrievalSources: [...meta.sources].sort(compareCodeUnits),
436
- graphExpanded: meta.graphExpanded,
440
+ retrievalRank: meta?.retrievalRank ?? index + 1,
441
+ ...(meta === undefined
442
+ ? {}
443
+ : {
444
+ retrievalSources: [...meta.sources].sort(compareCodeUnits),
445
+ graphExpanded: meta.graphExpanded,
446
+ }),
437
447
  contextIds:
438
448
  guidance.idsByResultIdentity.get(
439
449
  contextGuidanceResultIdentity(result)
@@ -486,7 +496,9 @@ export const planContextEvidence = async <T, P>(
486
496
  normalizeMaterialized(
487
497
  outcome.candidate,
488
498
  matchedFacets,
489
- plannedCandidate.retrievalRank
499
+ plannedCandidate.retrievalRank,
500
+ plannedCandidate.retrievalSources,
501
+ plannedCandidate.graphExpanded
490
502
  )
491
503
  );
492
504
  }
@@ -490,6 +490,12 @@ export const toContextCapsuleEvidence = (
490
490
  text: candidate.text,
491
491
  retrievalRank: candidate.retrievalRank,
492
492
  selectionRank,
493
+ ...(candidate.retrievalSources === undefined
494
+ ? {}
495
+ : { retrievalSources: [...candidate.retrievalSources] }),
496
+ ...(candidate.graphExpanded === undefined
497
+ ? {}
498
+ : { graphExpanded: candidate.graphExpanded }),
493
499
  facets: [...candidate.facets],
494
500
  };
495
501
  };
@@ -130,6 +130,9 @@ export class RetrievalTraceEvidenceOrigins {
130
130
  for (const run of runs.filter(({ kind }) => kind === "get")) {
131
131
  this.addStoredFallback(run.runId, run.payload);
132
132
  }
133
+ for (const run of runs.filter(({ kind }) => kind === "context")) {
134
+ this.addStoredFallback(run.runId, run.payload);
135
+ }
133
136
  }
134
137
 
135
138
  group(
@@ -128,7 +128,16 @@ const evidenceFromCapsule = (
128
128
  startLine: item.startLine,
129
129
  endLine: item.endLine,
130
130
  passageHash: item.passageHash,
131
- rank: item.retrievalRank,
131
+ ...(item.selectionRank === undefined ? {} : { rank: item.selectionRank }),
132
+ ...(item.retrievalRank === undefined
133
+ ? {}
134
+ : { plannerRank: item.retrievalRank }),
135
+ ...(item.retrievalSources === undefined
136
+ ? {}
137
+ : { sources: item.retrievalSources }),
138
+ ...(item.graphExpanded === undefined
139
+ ? {}
140
+ : { graphExpanded: item.graphExpanded }),
132
141
  }));
133
142
 
134
143
  export class RetrievalTraceSession {
@@ -324,7 +333,11 @@ export class RetrievalTraceSession {
324
333
  });
325
334
  if (!run.ok) return this.softenWriteFailure();
326
335
  this.persistedRecords += 1;
327
- return this.appendEvent("context", payload, runId);
336
+ const appended = await this.appendEvent("context", payload, runId);
337
+ if (appended.ok && appended.value !== "disabled") {
338
+ this.evidenceOrigins.addFallback(runId, payload.evidence);
339
+ }
340
+ return appended;
328
341
  }
329
342
 
330
343
  async recordEvidence(
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",