@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
@@ -21,6 +21,10 @@ import {
21
21
  SourcesTrigger,
22
22
  } from "../components/ai-elements/sources";
23
23
  import { AIModelSelector } from "../components/AIModelSelector";
24
+ import {
25
+ type AskVerification,
26
+ AskVerificationPanel,
27
+ } from "../components/AskVerificationPanel";
24
28
  import {
25
29
  ThoroughnessSelector,
26
30
  type Thoroughness,
@@ -57,6 +61,7 @@ interface PageProps {
57
61
  }
58
62
 
59
63
  interface Citation {
64
+ evidenceId?: string;
60
65
  docid: string;
61
66
  uri: string;
62
67
  startLine?: number;
@@ -88,12 +93,15 @@ interface AskResponse {
88
93
  vectorsUsed: boolean;
89
94
  answerGenerated: boolean;
90
95
  totalResults: number;
96
+ verificationRequested?: boolean;
97
+ abstained?: boolean;
91
98
  queryModes?: {
92
99
  term: number;
93
100
  intent: number;
94
101
  hyde: boolean;
95
102
  };
96
103
  };
104
+ verification?: AskVerification;
97
105
  }
98
106
 
99
107
  interface Capabilities {
@@ -139,7 +147,7 @@ function renderAnswer(
139
147
  const parts: React.ReactNode[] = [];
140
148
  let key = 0;
141
149
 
142
- const citationRegex = /\[(\d+)\]/g;
150
+ const citationRegex = /\[(?:evidence:([a-f0-9]{64})|(\d+))\]/g;
143
151
  let lastIndex = 0;
144
152
  let match: RegExpExecArray | null;
145
153
 
@@ -149,8 +157,13 @@ function renderAnswer(
149
157
  parts.push(answer.slice(lastIndex, match.index));
150
158
  }
151
159
 
152
- const citationNum = Number(match[1]);
153
- const citation = citations[citationNum - 1];
160
+ const evidenceId = match[1];
161
+ const numericCitation = match[2] ? Number(match[2]) : undefined;
162
+ const citationIndex = evidenceId
163
+ ? citations.findIndex((citation) => citation.evidenceId === evidenceId)
164
+ : (numericCitation ?? 0) - 1;
165
+ const citationNum = citationIndex + 1;
166
+ const citation = citations[citationIndex];
154
167
 
155
168
  if (citation) {
156
169
  parts.push(
@@ -186,6 +199,7 @@ export default function Ask({ navigate }: PageProps) {
186
199
  const [collections, setCollections] = useState<Collection[]>([]);
187
200
  const [thoroughness, setThoroughness] = useState<Thoroughness>("balanced");
188
201
  const [activePreset, setActivePreset] = useState("slim-tuned");
202
+ const [verify, setVerify] = useState(false);
189
203
 
190
204
  const [showAdvanced, setShowAdvanced] = useState(false);
191
205
  const [selectedCollection, setSelectedCollection] = useState("");
@@ -318,6 +332,7 @@ export default function Ask({ navigate }: PageProps) {
318
332
  const requestBody: Record<string, unknown> = {
319
333
  query: currentQuery,
320
334
  limit: 5,
335
+ verify,
321
336
  };
322
337
 
323
338
  if (selectedCollection) {
@@ -394,6 +409,7 @@ export default function Ask({ navigate }: PageProps) {
394
409
  tagsInput,
395
410
  thoroughness,
396
411
  until,
412
+ verify,
397
413
  ]
398
414
  );
399
415
 
@@ -443,6 +459,7 @@ export default function Ask({ navigate }: PageProps) {
443
459
  parseTagsCsv(tagsInput).length > 0
444
460
  ? `${tagMode}:${parseTagsCsv(tagsInput).join(",")}`
445
461
  : null,
462
+ verify ? "verified" : null,
446
463
  ].filter((pill): pill is string => Boolean(pill));
447
464
 
448
465
  return (
@@ -475,6 +492,16 @@ export default function Ask({ navigate }: PageProps) {
475
492
  value={thoroughness}
476
493
  />
477
494
 
495
+ <Button
496
+ aria-pressed={verify}
497
+ onClick={() => setVerify((value) => !value)}
498
+ size="sm"
499
+ type="button"
500
+ variant={verify ? "default" : "outline"}
501
+ >
502
+ Verify
503
+ </Button>
504
+
478
505
  <div className="h-6 w-px bg-border/40" />
479
506
 
480
507
  <AIModelSelector onPresetChange={setActivePreset} />
@@ -878,8 +905,19 @@ export default function Ask({ navigate }: PageProps) {
878
905
  </Sources>
879
906
  )}
880
907
 
908
+ {entry.response.verification && (
909
+ <AskVerificationPanel
910
+ navigate={navigate}
911
+ verification={entry.response.verification}
912
+ />
913
+ )}
914
+
881
915
  <div className="flex items-center gap-2 text-muted-foreground/60 text-xs">
882
- <span>{entry.response.results.length} results</span>
916
+ <span>
917
+ {entry.response.verification
918
+ ? `${entry.response.verification.capsule.evidence.length} evidence spans`
919
+ : `${entry.response.results.length} results`}
920
+ </span>
883
921
  {entry.response.meta.vectorsUsed && (
884
922
  <Badge
885
923
  className="font-mono text-[9px]"
@@ -36,6 +36,7 @@ import type { StartJobError } from "../jobs";
36
36
  import type { ResidentStatus } from "../status-model";
37
37
  import type { CollectionWatchService } from "../watch-service";
38
38
 
39
+ import { buildVerifiedAsk } from "../../app/verified-ask";
39
40
  import { modelsPull } from "../../cli/commands/models/pull";
40
41
  import {
41
42
  addCollection,
@@ -125,6 +126,7 @@ import {
125
126
  resolveModelUri,
126
127
  } from "../../llm/registry";
127
128
  import {
129
+ answerTraceTerminalStatus,
128
130
  generateGroundedAnswer,
129
131
  processAnswerResultWithTrace,
130
132
  } from "../../pipeline/answer";
@@ -289,14 +291,46 @@ export interface AskRequestBody {
289
291
  category?: string;
290
292
  author?: string;
291
293
  maxAnswerTokens?: number;
294
+ verify?: boolean;
295
+ contextBudgetTokens?: number;
296
+ contextBudgetBytes?: number;
297
+ minScore?: number;
292
298
  noExpand?: boolean;
293
299
  noRerank?: boolean;
300
+ graph?: boolean;
301
+ noGraph?: boolean;
294
302
  /** Comma-separated tags - filter to docs having ALL (AND) */
295
303
  tagsAll?: string;
296
304
  /** Comma-separated tags - filter to docs having ANY (OR) */
297
305
  tagsAny?: string;
298
306
  }
299
307
 
308
+ const ASK_REQUEST_KEYS = new Set<keyof AskRequestBody>([
309
+ "query",
310
+ "limit",
311
+ "collection",
312
+ "lang",
313
+ "intent",
314
+ "candidateLimit",
315
+ "exclude",
316
+ "queryModes",
317
+ "since",
318
+ "until",
319
+ "category",
320
+ "author",
321
+ "maxAnswerTokens",
322
+ "verify",
323
+ "contextBudgetTokens",
324
+ "contextBudgetBytes",
325
+ "minScore",
326
+ "noExpand",
327
+ "noRerank",
328
+ "graph",
329
+ "noGraph",
330
+ "tagsAll",
331
+ "tagsAny",
332
+ ]);
333
+
300
334
  export interface CreateCollectionRequestBody {
301
335
  path: string;
302
336
  name?: string;
@@ -512,11 +546,12 @@ async function startRestTrace(
512
546
  async function finishRestTrace(
513
547
  request: Request,
514
548
  session: RetrievalTraceSession | null,
515
- status: "completed" | "partial" | "failed",
549
+ status: "completed" | "partial" | "failed" | "cancelled",
516
550
  response: Response
517
551
  ): Promise<Response> {
518
552
  if (!session) return response;
519
- const terminalStatus = request.signal.aborted ? "cancelled" : status;
553
+ const terminalStatus =
554
+ request.signal.aborted || status === "cancelled" ? "cancelled" : status;
520
555
  const finished = await session.finish(terminalStatus);
521
556
  if (!finished.ok) {
522
557
  return withRetrievalTraceHeader(
@@ -3503,6 +3538,9 @@ export async function handleSearch(
3503
3538
  return errorResponse("VALIDATION", "Invalid JSON body");
3504
3539
  }
3505
3540
 
3541
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
3542
+ return errorResponse("VALIDATION", "Request body must be an object");
3543
+ }
3506
3544
  if (!body.query || typeof body.query !== "string") {
3507
3545
  return errorResponse("VALIDATION", "Missing or invalid query");
3508
3546
  }
@@ -4028,15 +4066,84 @@ export async function handleAsk(
4028
4066
  return errorResponse("VALIDATION", "Invalid JSON body");
4029
4067
  }
4030
4068
 
4069
+ if (body === null || typeof body !== "object" || Array.isArray(body)) {
4070
+ return errorResponse("VALIDATION", "Request body must be an object");
4071
+ }
4072
+ const unknownKey = Object.keys(body).find(
4073
+ (key) => !ASK_REQUEST_KEYS.has(key as keyof AskRequestBody)
4074
+ );
4075
+ if (unknownKey) {
4076
+ return errorResponse(
4077
+ "VALIDATION",
4078
+ `Unknown Ask request field: ${unknownKey}`
4079
+ );
4080
+ }
4031
4081
  if (!body.query || typeof body.query !== "string") {
4032
4082
  return errorResponse("VALIDATION", "Missing or invalid query");
4033
4083
  }
4034
4084
 
4085
+ for (const field of [
4086
+ "verify",
4087
+ "noExpand",
4088
+ "noRerank",
4089
+ "graph",
4090
+ "noGraph",
4091
+ ] as const) {
4092
+ if (body[field] !== undefined && typeof body[field] !== "boolean") {
4093
+ return errorResponse("VALIDATION", `${field} must be a boolean`);
4094
+ }
4095
+ }
4096
+ for (const [field, value, maximum] of [
4097
+ ["limit", body.limit, 20],
4098
+ ["candidateLimit", body.candidateLimit, 100],
4099
+ ["maxAnswerTokens", body.maxAnswerTokens, Number.MAX_SAFE_INTEGER],
4100
+ ] as const) {
4101
+ if (
4102
+ value !== undefined &&
4103
+ (!Number.isSafeInteger(value) || value < 1 || value > maximum)
4104
+ ) {
4105
+ return errorResponse(
4106
+ "VALIDATION",
4107
+ `${field} must be a positive integer${maximum < Number.MAX_SAFE_INTEGER ? ` no greater than ${maximum}` : ""}`
4108
+ );
4109
+ }
4110
+ }
4111
+
4035
4112
  const rawQuery = body.query.trim();
4036
4113
  if (!rawQuery) {
4037
4114
  return errorResponse("VALIDATION", "Query cannot be empty");
4038
4115
  }
4039
4116
 
4117
+ if (
4118
+ body.minScore !== undefined &&
4119
+ (typeof body.minScore !== "number" ||
4120
+ !Number.isFinite(body.minScore) ||
4121
+ body.minScore < 0 ||
4122
+ body.minScore > 1)
4123
+ ) {
4124
+ return errorResponse("VALIDATION", "minScore must be between 0 and 1");
4125
+ }
4126
+ if (
4127
+ body.contextBudgetTokens !== undefined &&
4128
+ (!Number.isSafeInteger(body.contextBudgetTokens) ||
4129
+ body.contextBudgetTokens < 1)
4130
+ ) {
4131
+ return errorResponse(
4132
+ "VALIDATION",
4133
+ "contextBudgetTokens must be a positive integer"
4134
+ );
4135
+ }
4136
+ if (
4137
+ body.contextBudgetBytes !== undefined &&
4138
+ (!Number.isSafeInteger(body.contextBudgetBytes) ||
4139
+ body.contextBudgetBytes < 1)
4140
+ ) {
4141
+ return errorResponse(
4142
+ "VALIDATION",
4143
+ "contextBudgetBytes must be a positive integer"
4144
+ );
4145
+ }
4146
+
4040
4147
  // Parse tag filters
4041
4148
  let tagsAll: string[] | undefined;
4042
4149
  let tagsAny: string[] | undefined;
@@ -4128,6 +4235,7 @@ export async function handleAsk(
4128
4235
  collection: body.collection,
4129
4236
  lang: body.lang,
4130
4237
  intent: body.intent?.trim() || undefined,
4238
+ minScore: body.minScore,
4131
4239
  noExpand: body.noExpand,
4132
4240
  noRerank: body.noRerank,
4133
4241
  candidateLimit:
@@ -4135,6 +4243,8 @@ export async function handleAsk(
4135
4243
  ? Math.min(body.candidateLimit, 100)
4136
4244
  : undefined,
4137
4245
  exclude,
4246
+ graph: body.graph,
4247
+ noGraph: body.noGraph,
4138
4248
  queryModes: normalizedQueryModes,
4139
4249
  tagsAll,
4140
4250
  tagsAny,
@@ -4142,6 +4252,10 @@ export async function handleAsk(
4142
4252
  until: body.until,
4143
4253
  categories,
4144
4254
  author,
4255
+ verify: body.verify,
4256
+ contextBudgetTokens: body.contextBudgetTokens,
4257
+ contextBudgetBytes: body.contextBudgetBytes,
4258
+ maxAnswerTokens: body.maxAnswerTokens,
4145
4259
  };
4146
4260
  const trace = await startRestTrace(ctx, {
4147
4261
  query: normalizedQuery,
@@ -4166,7 +4280,7 @@ export async function handleAsk(
4166
4280
  return finishRestTrace(
4167
4281
  req,
4168
4282
  trace.session,
4169
- "failed",
4283
+ req.signal.aborted ? "cancelled" : "failed",
4170
4284
  errorResponse("RUNTIME", unavailable.error.message, 500)
4171
4285
  );
4172
4286
  }
@@ -4182,6 +4296,38 @@ export async function handleAsk(
4182
4296
  );
4183
4297
  }
4184
4298
 
4299
+ if (body.verify && ctx.answerPort) {
4300
+ try {
4301
+ const verified = await buildVerifiedAsk(normalizedQuery, askOptions, {
4302
+ store: ctx.store,
4303
+ config: ctx.config,
4304
+ indexName: ctx.indexName,
4305
+ vectorIndex: ctx.vectorIndex,
4306
+ embedPort: ctx.embedPort,
4307
+ rerankPort: ctx.rerankPort,
4308
+ genPort: ctx.answerPort,
4309
+ traceSession: trace.session ?? undefined,
4310
+ });
4311
+ return finishRestTrace(
4312
+ req,
4313
+ trace.session,
4314
+ answerTraceTerminalStatus(verified.citations),
4315
+ jsonResponse(verified)
4316
+ );
4317
+ } catch (error) {
4318
+ return finishRestTrace(
4319
+ req,
4320
+ trace.session,
4321
+ "failed",
4322
+ errorResponse(
4323
+ "RUNTIME",
4324
+ error instanceof Error ? error.message : String(error),
4325
+ 500
4326
+ )
4327
+ );
4328
+ }
4329
+ }
4330
+
4185
4331
  // Run hybrid search first
4186
4332
  const searchResult = await searchHybrid(
4187
4333
  {