@gmickel/gno 1.18.0 → 1.19.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 +5 -3
- package/assets/skill/SKILL.md +27 -0
- 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 +2894 -0
- package/spec/db/schema.sql +442 -0
- package/spec/evals-agentic.md +510 -0
- package/spec/evals.md +1106 -0
- package/spec/mcp.md +2229 -0
- package/spec/output-schemas/activation-verification.schema.json +515 -0
- package/spec/output-schemas/ask.schema.json +366 -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/collection-list.schema.json +45 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +691 -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-types.ts +3 -0
- package/src/app/context-runtime.ts +15 -1
- package/src/cli/commands/ask.ts +106 -36
- 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 +255 -0
- 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-compiler.ts +11 -4
- 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 +175 -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 +494 -0
- package/src/core/retrieval-trace.ts +472 -0
- package/src/mcp/tools/context.ts +59 -8
- package/src/mcp/tools/get.ts +35 -1
- package/src/mcp/tools/index.ts +74 -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/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 +43 -0
- package/src/pipeline/vsearch.ts +101 -38
- package/src/sdk/client.ts +380 -71
- 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/globals.built.css +1 -1
- package/src/serve/public/lib/workspace-tabs.ts +2 -0
- 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 +366 -72
- 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/mcp/tools/context.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** MCP Context Capsule tools over the shared application runtime. */
|
|
2
2
|
|
|
3
|
+
import type { RetrievalTraceSession } from "../../core/retrieval-trace-session";
|
|
3
4
|
import type { ModelLease } from "../../llm/nodeLlamaCpp/lifecycle";
|
|
4
5
|
import type { EmbeddingPort, RerankPort } from "../../llm/types";
|
|
5
6
|
import type { VectorIndexPort } from "../../store/vector";
|
|
@@ -20,6 +21,10 @@ import {
|
|
|
20
21
|
parseContextVerifySurfaceInput,
|
|
21
22
|
} from "../../app/context-surface";
|
|
22
23
|
import { createNonTtyProgressRenderer } from "../../cli/progress";
|
|
24
|
+
import {
|
|
25
|
+
finishRetrievalTraceAfterError,
|
|
26
|
+
startRetrievalTraceRequest,
|
|
27
|
+
} from "../../core/retrieval-trace-request";
|
|
23
28
|
import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
|
|
24
29
|
import { resolveDownloadPolicy } from "../../llm/policy";
|
|
25
30
|
import { resolveModelUri } from "../../llm/registry";
|
|
@@ -28,11 +33,15 @@ import { createVectorIndexPort } from "../../store/vector";
|
|
|
28
33
|
interface ContextToolResultData {
|
|
29
34
|
structuredContent: Record<string, unknown>;
|
|
30
35
|
text: string;
|
|
36
|
+
traceId?: string;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
39
|
const asToolResult = (data: ContextToolResultData): ToolResult => ({
|
|
34
40
|
content: [{ type: "text", text: data.text }],
|
|
35
41
|
structuredContent: data.structuredContent,
|
|
42
|
+
...(data.traceId
|
|
43
|
+
? { _meta: { gno: { retrievalTrace: { traceId: data.traceId } } } }
|
|
44
|
+
: {}),
|
|
36
45
|
});
|
|
37
46
|
|
|
38
47
|
const asToolError = (error: unknown): ToolResult => {
|
|
@@ -180,15 +189,50 @@ export const handleContext = (
|
|
|
180
189
|
context.config.collections.map((collection) => collection.name)
|
|
181
190
|
);
|
|
182
191
|
const useModels = parsed.input.depthPolicy !== "fast";
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
const collection =
|
|
193
|
+
parsed.input.collections?.length === 1
|
|
194
|
+
? parsed.input.collections[0]
|
|
195
|
+
: undefined;
|
|
196
|
+
const modelUris = useModels
|
|
197
|
+
? [
|
|
198
|
+
resolveModelUri(context.config, "embed", undefined, collection),
|
|
199
|
+
resolveModelUri(context.config, "rerank", undefined, collection),
|
|
200
|
+
]
|
|
201
|
+
: [];
|
|
202
|
+
let modelPorts: Awaited<ReturnType<typeof createMcpModelPorts>> | null =
|
|
203
|
+
null;
|
|
204
|
+
let traceSession: RetrievalTraceSession | undefined;
|
|
191
205
|
try {
|
|
206
|
+
const traceStart = await startRetrievalTraceRequest({
|
|
207
|
+
store: context.store,
|
|
208
|
+
config: context.config,
|
|
209
|
+
query: parsed.input.query ?? parsed.input.goal,
|
|
210
|
+
goal: parsed.input.goal,
|
|
211
|
+
filters: {
|
|
212
|
+
limit: parsed.input.limit,
|
|
213
|
+
collection,
|
|
214
|
+
collections: [...(parsed.input.collections ?? [])].sort(),
|
|
215
|
+
lang: parsed.input.lang,
|
|
216
|
+
tagsAll: parsed.input.tagsAll,
|
|
217
|
+
tagsAny: parsed.input.tagsAny,
|
|
218
|
+
since: parsed.input.since,
|
|
219
|
+
until: parsed.input.until,
|
|
220
|
+
categories: parsed.input.categories,
|
|
221
|
+
author: parsed.input.author,
|
|
222
|
+
graph: parsed.input.graph,
|
|
223
|
+
candidateLimit: parsed.input.candidateLimit,
|
|
224
|
+
queryModes: parsed.input.queryModes,
|
|
225
|
+
uriPrefix: parsed.input.uriPrefix ?? undefined,
|
|
226
|
+
},
|
|
227
|
+
pipeline: "context",
|
|
228
|
+
indexName: context.indexName,
|
|
229
|
+
modelUris,
|
|
230
|
+
});
|
|
231
|
+
if (!traceStart.ok) throw new Error(traceStart.error.message);
|
|
232
|
+
traceSession = traceStart.value ?? undefined;
|
|
233
|
+
modelPorts = useModels
|
|
234
|
+
? await createMcpModelPorts(context, collection)
|
|
235
|
+
: null;
|
|
192
236
|
const capsule = await buildContextCapsule(parsed.input, {
|
|
193
237
|
store: context.store,
|
|
194
238
|
config: context.config,
|
|
@@ -196,14 +240,21 @@ export const handleContext = (
|
|
|
196
240
|
vectorIndex: modelPorts?.vectorIndex ?? null,
|
|
197
241
|
embedPort: modelPorts?.embedPort ?? null,
|
|
198
242
|
rerankPort: modelPorts?.rerankPort ?? null,
|
|
243
|
+
traceSession,
|
|
199
244
|
});
|
|
245
|
+
const finalized = await traceSession?.finish("completed");
|
|
246
|
+
if (finalized && !finalized.ok) throw new Error(finalized.error.message);
|
|
200
247
|
return {
|
|
201
248
|
structuredContent: capsule as unknown as Record<string, unknown>,
|
|
202
249
|
// MCP model context receives this projection exactly once. The full
|
|
203
250
|
// canonical capsule remains available to application clients through
|
|
204
251
|
// structuredContent and is deliberately not duplicated in text.
|
|
205
252
|
text: formatContextCapsuleAgentJson(capsule),
|
|
253
|
+
traceId: traceSession?.metadata()?.traceId,
|
|
206
254
|
};
|
|
255
|
+
} catch (cause) {
|
|
256
|
+
await finishRetrievalTraceAfterError(traceSession, cause);
|
|
257
|
+
throw cause;
|
|
207
258
|
} finally {
|
|
208
259
|
await modelPorts?.dispose();
|
|
209
260
|
}
|
package/src/mcp/tools/get.ts
CHANGED
|
@@ -16,6 +16,11 @@ import {
|
|
|
16
16
|
} from "../../core/document-capabilities";
|
|
17
17
|
import { resolveEffectiveIndex } from "../../core/indexed-reference";
|
|
18
18
|
import { parseRef } from "../../core/ref-parser";
|
|
19
|
+
import {
|
|
20
|
+
attachRetrievalTraceMetadata,
|
|
21
|
+
evidenceFromExactDocument,
|
|
22
|
+
RetrievalTraceSession,
|
|
23
|
+
} from "../../core/retrieval-trace-session";
|
|
19
24
|
import { openScopedIndexStore } from "../../store/sqlite/scoped-index";
|
|
20
25
|
import { runTool, type ToolResult } from "./index";
|
|
21
26
|
|
|
@@ -24,6 +29,7 @@ interface GetInput {
|
|
|
24
29
|
fromLine?: number;
|
|
25
30
|
lineCount?: number;
|
|
26
31
|
lineNumbers?: boolean;
|
|
32
|
+
traceId?: string;
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
interface GetResponse {
|
|
@@ -165,6 +171,7 @@ export function handleGet(
|
|
|
165
171
|
|
|
166
172
|
// Apply line range if specified
|
|
167
173
|
let content = fullContent;
|
|
174
|
+
let exactContent = fullContent;
|
|
168
175
|
let returnedLines: { start: number; end: number } | undefined;
|
|
169
176
|
|
|
170
177
|
// lineNumbers defaults to true per spec
|
|
@@ -176,12 +183,14 @@ export function handleGet(
|
|
|
176
183
|
if (startLine > totalLines) {
|
|
177
184
|
// Return empty content for out-of-range request
|
|
178
185
|
content = "";
|
|
186
|
+
exactContent = "";
|
|
179
187
|
returnedLines = undefined;
|
|
180
188
|
} else {
|
|
181
189
|
const count = args.lineCount ?? totalLines - startLine + 1;
|
|
182
190
|
const endLine = Math.min(startLine + count - 1, totalLines);
|
|
183
191
|
|
|
184
192
|
const slicedLines = contentLines.slice(startLine - 1, endLine);
|
|
193
|
+
exactContent = slicedLines.join("\n");
|
|
185
194
|
|
|
186
195
|
if (showLineNumbers) {
|
|
187
196
|
content = slicedLines
|
|
@@ -242,7 +251,32 @@ export function handleGet(
|
|
|
242
251
|
}),
|
|
243
252
|
};
|
|
244
253
|
|
|
245
|
-
return response;
|
|
254
|
+
if (!args.traceId) return response;
|
|
255
|
+
const resumed = await RetrievalTraceSession.resume({
|
|
256
|
+
store: scoped.store,
|
|
257
|
+
config: ctx.config.retrievalTraces,
|
|
258
|
+
traceId: args.traceId,
|
|
259
|
+
});
|
|
260
|
+
if (!resumed.ok) throw new Error(resumed.error.message);
|
|
261
|
+
const traceSession = resumed.value;
|
|
262
|
+
if (!traceSession) return response;
|
|
263
|
+
const lines = returnedLines ?? { start: 1, end: totalLines };
|
|
264
|
+
const evidence = evidenceFromExactDocument({
|
|
265
|
+
docid: response.docid,
|
|
266
|
+
uri: response.uri,
|
|
267
|
+
sourceHash: response.source.sourceHash,
|
|
268
|
+
mirrorHash: response.conversion?.mirrorHash,
|
|
269
|
+
content: exactContent,
|
|
270
|
+
startLine: lines.start,
|
|
271
|
+
endLine: lines.end,
|
|
272
|
+
});
|
|
273
|
+
if (evidence) {
|
|
274
|
+
const got = await traceSession.recordEvidence("get", [evidence]);
|
|
275
|
+
if (!got.ok) throw new Error(got.error.message);
|
|
276
|
+
const opened = await traceSession.recordEvidence("open", [evidence]);
|
|
277
|
+
if (!opened.ok) throw new Error(opened.error.message);
|
|
278
|
+
}
|
|
279
|
+
return attachRetrievalTraceMetadata(response, traceSession);
|
|
246
280
|
} finally {
|
|
247
281
|
await scoped.close();
|
|
248
282
|
}
|