@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.
- package/README.md +14 -7
- package/assets/skill/SKILL.md +54 -12
- package/assets/skill/mcp-reference.md +7 -2
- package/assets/skill/recipes/citation-and-provenance.md +32 -9
- package/package.json +2 -1
- package/spec/AGENTS.md +83 -0
- package/spec/CLAUDE.md +83 -0
- package/spec/bench-fixture.schema.json +137 -0
- package/spec/cli.md +2919 -0
- package/spec/db/schema.sql +442 -0
- package/spec/evals-agentic.md +592 -0
- package/spec/evals.md +1106 -0
- package/spec/mcp.md +2279 -0
- package/spec/output-schemas/activation-verification.schema.json +515 -0
- package/spec/output-schemas/ask.schema.json +564 -0
- package/spec/output-schemas/backlinks.schema.json +131 -0
- package/spec/output-schemas/bench-result.schema.json +120 -0
- package/spec/output-schemas/capture-receipt.schema.json +143 -0
- package/spec/output-schemas/claim-verification.schema.json +291 -0
- package/spec/output-schemas/collection-list.schema.json +45 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +726 -0
- package/spec/output-schemas/context-capsule-verification.schema.json +1338 -0
- package/spec/output-schemas/context-list.schema.json +21 -0
- package/spec/output-schemas/doctor.schema.json +313 -0
- package/spec/output-schemas/error.schema.json +30 -0
- package/spec/output-schemas/expansion.schema.json +37 -0
- package/spec/output-schemas/get.schema.json +140 -0
- package/spec/output-schemas/graph-query.schema.json +99 -0
- package/spec/output-schemas/graph.schema.json +371 -0
- package/spec/output-schemas/links-list.schema.json +186 -0
- package/spec/output-schemas/mcp-add-collection-result.schema.json +23 -0
- package/spec/output-schemas/mcp-capture-result.schema.json +152 -0
- package/spec/output-schemas/mcp-http-error.schema.json +30 -0
- package/spec/output-schemas/mcp-job-list.schema.json +58 -0
- package/spec/output-schemas/mcp-job-status.schema.json +224 -0
- package/spec/output-schemas/mcp-remove-result.schema.json +39 -0
- package/spec/output-schemas/mcp-sync-result.schema.json +41 -0
- package/spec/output-schemas/mcp-tag-result.schema.json +33 -0
- package/spec/output-schemas/models-list.schema.json +93 -0
- package/spec/output-schemas/multi-get.schema.json +103 -0
- package/spec/output-schemas/process-status.schema.json +119 -0
- package/spec/output-schemas/query-diagnose.schema.json +123 -0
- package/spec/output-schemas/resident-status.schema.json +154 -0
- package/spec/output-schemas/retrieval-trace-common.schema.json +492 -0
- package/spec/output-schemas/retrieval-trace-delete.schema.json +16 -0
- package/spec/output-schemas/retrieval-trace-export.schema.json +61 -0
- package/spec/output-schemas/retrieval-trace-filters.schema.json +139 -0
- package/spec/output-schemas/retrieval-trace-judgment.schema.json +15 -0
- package/spec/output-schemas/retrieval-trace-list.schema.json +18 -0
- package/spec/output-schemas/retrieval-trace-payloads.schema.json +178 -0
- package/spec/output-schemas/retrieval-trace-purge.schema.json +31 -0
- package/spec/output-schemas/retrieval-trace-qrels.schema.json +303 -0
- package/spec/output-schemas/retrieval-trace-replay.schema.json +286 -0
- package/spec/output-schemas/retrieval-trace-show.schema.json +69 -0
- package/spec/output-schemas/retrieval-trace-summary.schema.json +65 -0
- package/spec/output-schemas/search-result.schema.json +154 -0
- package/spec/output-schemas/search-results.schema.json +338 -0
- package/spec/output-schemas/similar.schema.json +84 -0
- package/spec/output-schemas/status.schema.json +676 -0
- package/spec/output-schemas/tags-list.schema.json +48 -0
- package/src/app/context-runtime-contract.ts +10 -5
- package/src/app/context-runtime-input.ts +29 -1
- package/src/app/context-runtime-types.ts +7 -0
- package/src/app/context-runtime.ts +20 -2
- package/src/app/context-surface.ts +4 -0
- package/src/app/verified-ask.ts +291 -0
- package/src/cli/commands/ask-format.ts +255 -0
- package/src/cli/commands/ask.ts +144 -183
- package/src/cli/commands/context-build.ts +56 -9
- package/src/cli/commands/get.ts +64 -3
- package/src/cli/commands/query.ts +62 -23
- package/src/cli/commands/replay.ts +140 -0
- package/src/cli/commands/search.ts +48 -3
- package/src/cli/commands/shared.ts +3 -1
- package/src/cli/commands/trace.ts +200 -0
- package/src/cli/commands/vsearch.ts +75 -53
- package/src/cli/program.ts +287 -1
- package/src/config/index.ts +9 -0
- package/src/config/retrieval-traces.ts +56 -0
- package/src/config/types.ts +4 -0
- package/src/core/context-budget.ts +6 -0
- package/src/core/context-capsule-retrieval-schema.ts +4 -0
- package/src/core/context-capsule-schema.ts +17 -0
- package/src/core/context-capsule-validation.ts +3 -2
- package/src/core/context-capsule.ts +18 -0
- package/src/core/context-compiler.ts +44 -25
- package/src/core/context-evidence.ts +6 -0
- package/src/core/retrieval-qrels.ts +405 -0
- package/src/core/retrieval-replay-candidate.ts +368 -0
- package/src/core/retrieval-replay-types.ts +109 -0
- package/src/core/retrieval-replay-validation.ts +89 -0
- package/src/core/retrieval-replay.ts +441 -0
- package/src/core/retrieval-trace-evidence-origin.ts +178 -0
- package/src/core/retrieval-trace-export.ts +113 -0
- package/src/core/retrieval-trace-filter-normalization.ts +27 -0
- package/src/core/retrieval-trace-filters.ts +19 -0
- package/src/core/retrieval-trace-management-helpers.ts +247 -0
- package/src/core/retrieval-trace-management-types.ts +132 -0
- package/src/core/retrieval-trace-management.ts +422 -0
- package/src/core/retrieval-trace-request.ts +141 -0
- package/src/core/retrieval-trace-session.ts +507 -0
- package/src/core/retrieval-trace.ts +472 -0
- package/src/llm/errors.ts +10 -1
- package/src/llm/httpGeneration.ts +11 -1
- package/src/llm/nodeLlamaCpp/generation.ts +54 -10
- package/src/llm/types.ts +6 -0
- package/src/mcp/tools/ask.ts +228 -0
- package/src/mcp/tools/context.ts +87 -15
- package/src/mcp/tools/get.ts +35 -1
- package/src/mcp/tools/index.ts +83 -0
- package/src/mcp/tools/query.ts +95 -64
- package/src/mcp/tools/search.ts +36 -13
- package/src/mcp/tools/trace.ts +143 -0
- package/src/mcp/tools/vsearch.ts +71 -38
- package/src/pipeline/answer.ts +167 -26
- package/src/pipeline/claim-verification-schema.ts +235 -0
- package/src/pipeline/claim-verification.ts +487 -0
- package/src/pipeline/claim-verifier.ts +474 -0
- package/src/pipeline/graph-retrieval.ts +15 -1
- package/src/pipeline/hybrid.ts +151 -43
- package/src/pipeline/search.ts +36 -3
- package/src/pipeline/trace-metadata.ts +47 -0
- package/src/pipeline/types.ts +68 -0
- package/src/pipeline/vsearch.ts +101 -38
- package/src/sdk/client.ts +415 -73
- package/src/sdk/documents.ts +48 -1
- package/src/sdk/index.ts +17 -0
- package/src/sdk/types.ts +28 -0
- package/src/serve/context-capsule.ts +67 -8
- package/src/serve/public/app.tsx +12 -1
- package/src/serve/public/components/AskVerificationPanel.tsx +189 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/lib/workspace-tabs.ts +2 -0
- package/src/serve/public/pages/Ask.tsx +42 -4
- package/src/serve/public/pages/Dashboard.tsx +10 -0
- package/src/serve/public/pages/TraceHistory.tsx +478 -0
- package/src/serve/public/pages/trace-history-detail.tsx +224 -0
- package/src/serve/retrieval-trace.ts +28 -0
- package/src/serve/routes/api.ts +508 -68
- package/src/serve/routes/traces.ts +156 -0
- package/src/serve/server.ts +87 -2
- package/src/store/index.ts +31 -0
- package/src/store/migrations/014-retrieval-traces.ts +303 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/retrieval-trace-codec.ts +384 -0
- package/src/store/sqlite/adapter.ts +153 -1
- package/src/store/sqlite/retrieval-trace-management-store.ts +341 -0
- package/src/store/sqlite/retrieval-trace-retention.ts +349 -0
- package/src/store/sqlite/retrieval-trace-rows.ts +267 -0
- package/src/store/sqlite/retrieval-trace-store.ts +515 -0
- package/src/store/types.ts +297 -0
- package/src/store/vector/sqlite-vec.ts +76 -1
- 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 {
|
|
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
|
-
|
|
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
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
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
|
}
|