@gmickel/gno 1.17.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.
Files changed (167) hide show
  1. package/README.md +18 -4
  2. package/assets/skill/SKILL.md +44 -1
  3. package/assets/skill/mcp-reference.md +21 -0
  4. package/package.json +3 -2
  5. package/spec/AGENTS.md +83 -0
  6. package/spec/CLAUDE.md +83 -0
  7. package/spec/bench-fixture.schema.json +137 -0
  8. package/spec/cli.md +2894 -0
  9. package/spec/db/schema.sql +442 -0
  10. package/spec/evals-agentic.md +510 -0
  11. package/spec/evals.md +1106 -0
  12. package/spec/mcp.md +2229 -0
  13. package/spec/output-schemas/activation-verification.schema.json +515 -0
  14. package/spec/output-schemas/ask.schema.json +366 -0
  15. package/spec/output-schemas/backlinks.schema.json +131 -0
  16. package/spec/output-schemas/bench-result.schema.json +120 -0
  17. package/spec/output-schemas/capture-receipt.schema.json +143 -0
  18. package/spec/output-schemas/collection-list.schema.json +45 -0
  19. package/spec/output-schemas/context-capsule-v1.schema.json +691 -0
  20. package/spec/output-schemas/context-capsule-verification.schema.json +1338 -0
  21. package/spec/output-schemas/context-list.schema.json +21 -0
  22. package/spec/output-schemas/doctor.schema.json +313 -0
  23. package/spec/output-schemas/error.schema.json +30 -0
  24. package/spec/output-schemas/expansion.schema.json +37 -0
  25. package/spec/output-schemas/get.schema.json +140 -0
  26. package/spec/output-schemas/graph-query.schema.json +99 -0
  27. package/spec/output-schemas/graph.schema.json +371 -0
  28. package/spec/output-schemas/links-list.schema.json +186 -0
  29. package/spec/output-schemas/mcp-add-collection-result.schema.json +23 -0
  30. package/spec/output-schemas/mcp-capture-result.schema.json +152 -0
  31. package/spec/output-schemas/mcp-http-error.schema.json +30 -0
  32. package/spec/output-schemas/mcp-job-list.schema.json +58 -0
  33. package/spec/output-schemas/mcp-job-status.schema.json +224 -0
  34. package/spec/output-schemas/mcp-remove-result.schema.json +39 -0
  35. package/spec/output-schemas/mcp-sync-result.schema.json +41 -0
  36. package/spec/output-schemas/mcp-tag-result.schema.json +33 -0
  37. package/spec/output-schemas/models-list.schema.json +93 -0
  38. package/spec/output-schemas/multi-get.schema.json +103 -0
  39. package/spec/output-schemas/process-status.schema.json +119 -0
  40. package/spec/output-schemas/query-diagnose.schema.json +123 -0
  41. package/spec/output-schemas/resident-status.schema.json +154 -0
  42. package/spec/output-schemas/retrieval-trace-common.schema.json +492 -0
  43. package/spec/output-schemas/retrieval-trace-delete.schema.json +16 -0
  44. package/spec/output-schemas/retrieval-trace-export.schema.json +61 -0
  45. package/spec/output-schemas/retrieval-trace-filters.schema.json +139 -0
  46. package/spec/output-schemas/retrieval-trace-judgment.schema.json +15 -0
  47. package/spec/output-schemas/retrieval-trace-list.schema.json +18 -0
  48. package/spec/output-schemas/retrieval-trace-payloads.schema.json +178 -0
  49. package/spec/output-schemas/retrieval-trace-purge.schema.json +31 -0
  50. package/spec/output-schemas/retrieval-trace-qrels.schema.json +303 -0
  51. package/spec/output-schemas/retrieval-trace-replay.schema.json +286 -0
  52. package/spec/output-schemas/retrieval-trace-show.schema.json +69 -0
  53. package/spec/output-schemas/retrieval-trace-summary.schema.json +65 -0
  54. package/spec/output-schemas/search-result.schema.json +154 -0
  55. package/spec/output-schemas/search-results.schema.json +338 -0
  56. package/spec/output-schemas/similar.schema.json +84 -0
  57. package/spec/output-schemas/status.schema.json +676 -0
  58. package/spec/output-schemas/tags-list.schema.json +48 -0
  59. package/src/app/context-runtime-types.ts +3 -0
  60. package/src/app/context-runtime.ts +15 -1
  61. package/src/cli/commands/ask.ts +106 -36
  62. package/src/cli/commands/context-build.ts +56 -9
  63. package/src/cli/commands/daemon.ts +69 -2
  64. package/src/cli/commands/get.ts +64 -3
  65. package/src/cli/commands/models/pull.ts +13 -3
  66. package/src/cli/commands/query.ts +62 -23
  67. package/src/cli/commands/replay.ts +140 -0
  68. package/src/cli/commands/search.ts +48 -3
  69. package/src/cli/commands/shared.ts +3 -1
  70. package/src/cli/commands/status.ts +2 -0
  71. package/src/cli/commands/trace.ts +200 -0
  72. package/src/cli/commands/vsearch.ts +75 -53
  73. package/src/cli/detach.ts +37 -20
  74. package/src/cli/program.ts +329 -27
  75. package/src/config/index.ts +12 -0
  76. package/src/config/retrieval-traces.ts +56 -0
  77. package/src/config/types.ts +41 -0
  78. package/src/core/context-compiler.ts +11 -4
  79. package/src/core/job-manager.ts +19 -0
  80. package/src/core/mutation-generations.ts +33 -0
  81. package/src/core/retrieval-qrels.ts +405 -0
  82. package/src/core/retrieval-replay-candidate.ts +368 -0
  83. package/src/core/retrieval-replay-types.ts +109 -0
  84. package/src/core/retrieval-replay-validation.ts +89 -0
  85. package/src/core/retrieval-replay.ts +441 -0
  86. package/src/core/retrieval-trace-evidence-origin.ts +175 -0
  87. package/src/core/retrieval-trace-export.ts +113 -0
  88. package/src/core/retrieval-trace-filter-normalization.ts +27 -0
  89. package/src/core/retrieval-trace-filters.ts +19 -0
  90. package/src/core/retrieval-trace-management-helpers.ts +247 -0
  91. package/src/core/retrieval-trace-management-types.ts +132 -0
  92. package/src/core/retrieval-trace-management.ts +422 -0
  93. package/src/core/retrieval-trace-request.ts +141 -0
  94. package/src/core/retrieval-trace-session.ts +494 -0
  95. package/src/core/retrieval-trace.ts +472 -0
  96. package/src/llm/cache.ts +13 -3
  97. package/src/llm/nodeLlamaCpp/adapter.ts +10 -1
  98. package/src/llm/nodeLlamaCpp/lifecycle.ts +71 -0
  99. package/src/mcp/context.ts +161 -0
  100. package/src/mcp/http-security.ts +477 -0
  101. package/src/mcp/http-session.ts +272 -0
  102. package/src/mcp/http-transport.ts +370 -0
  103. package/src/mcp/resources/index.ts +141 -134
  104. package/src/mcp/server.ts +19 -79
  105. package/src/mcp/tools/add-collection.ts +3 -1
  106. package/src/mcp/tools/capture.ts +3 -0
  107. package/src/mcp/tools/clear-collection-embeddings.ts +2 -0
  108. package/src/mcp/tools/context.ts +68 -16
  109. package/src/mcp/tools/embed.ts +62 -52
  110. package/src/mcp/tools/get.ts +35 -1
  111. package/src/mcp/tools/index-cmd.ts +88 -74
  112. package/src/mcp/tools/index.ts +96 -2
  113. package/src/mcp/tools/query.ts +95 -64
  114. package/src/mcp/tools/remove-collection.ts +2 -0
  115. package/src/mcp/tools/search.ts +36 -13
  116. package/src/mcp/tools/status.ts +11 -0
  117. package/src/mcp/tools/sync.ts +16 -14
  118. package/src/mcp/tools/trace.ts +143 -0
  119. package/src/mcp/tools/vsearch.ts +71 -38
  120. package/src/mcp/tools/workspace-write.ts +7 -3
  121. package/src/pipeline/answer.ts +167 -26
  122. package/src/pipeline/graph-retrieval.ts +15 -1
  123. package/src/pipeline/hybrid.ts +151 -43
  124. package/src/pipeline/search.ts +36 -3
  125. package/src/pipeline/trace-metadata.ts +47 -0
  126. package/src/pipeline/types.ts +43 -0
  127. package/src/pipeline/vsearch.ts +101 -38
  128. package/src/sdk/client.ts +380 -71
  129. package/src/sdk/documents.ts +48 -1
  130. package/src/sdk/index.ts +17 -0
  131. package/src/sdk/types.ts +28 -0
  132. package/src/serve/background-runtime.ts +12 -212
  133. package/src/serve/context-capsule.ts +67 -8
  134. package/src/serve/embed-scheduler.ts +74 -43
  135. package/src/serve/index.ts +9 -0
  136. package/src/serve/jobs.ts +78 -80
  137. package/src/serve/public/app.tsx +12 -1
  138. package/src/serve/public/components/HealthCenter.tsx +74 -1
  139. package/src/serve/public/globals.built.css +1 -1
  140. package/src/serve/public/lib/workspace-tabs.ts +2 -0
  141. package/src/serve/public/pages/Dashboard.tsx +11 -0
  142. package/src/serve/public/pages/TraceHistory.tsx +478 -0
  143. package/src/serve/public/pages/trace-history-detail.tsx +224 -0
  144. package/src/serve/resident-admission.ts +159 -0
  145. package/src/serve/resident-background-work.ts +39 -0
  146. package/src/serve/resident-request.ts +55 -0
  147. package/src/serve/resident-runtime.ts +490 -0
  148. package/src/serve/resident-status.ts +96 -0
  149. package/src/serve/retrieval-trace.ts +28 -0
  150. package/src/serve/routes/api.ts +629 -239
  151. package/src/serve/routes/mcp.ts +69 -0
  152. package/src/serve/routes/traces.ts +156 -0
  153. package/src/serve/server.ts +276 -37
  154. package/src/serve/status-model.ts +51 -0
  155. package/src/serve/status.ts +5 -0
  156. package/src/store/index.ts +31 -0
  157. package/src/store/migrations/014-retrieval-traces.ts +303 -0
  158. package/src/store/migrations/index.ts +2 -0
  159. package/src/store/retrieval-trace-codec.ts +384 -0
  160. package/src/store/sqlite/adapter.ts +179 -10
  161. package/src/store/sqlite/retrieval-trace-management-store.ts +341 -0
  162. package/src/store/sqlite/retrieval-trace-retention.ts +349 -0
  163. package/src/store/sqlite/retrieval-trace-rows.ts +267 -0
  164. package/src/store/sqlite/retrieval-trace-store.ts +515 -0
  165. package/src/store/types.ts +297 -0
  166. package/src/store/vector/sqlite-vec.ts +76 -1
  167. package/src/store/vector/types.ts +1 -1
@@ -35,6 +35,8 @@ export interface ModelsPullOptions {
35
35
  force?: boolean;
36
36
  /** Progress callback for UI (omit to disable progress) */
37
37
  onProgress?: (type: ModelType, progress: DownloadProgress) => void;
38
+ /** Stop before the next model and suppress subsequent work. */
39
+ signal?: AbortSignal;
38
40
  }
39
41
 
40
42
  export interface ModelPullResult {
@@ -52,6 +54,10 @@ export interface ModelsPullResult {
52
54
  skipped: number;
53
55
  }
54
56
 
57
+ export interface ModelsPullDependencies {
58
+ cache?: Pick<ModelCache, "download" | "getCachedPath" | "isCached">;
59
+ }
60
+
55
61
  // ─────────────────────────────────────────────────────────────────────────────
56
62
  // Implementation
57
63
  // ─────────────────────────────────────────────────────────────────────────────
@@ -87,7 +93,8 @@ function getTypesToPull(options: ModelsPullOptions): ModelType[] {
87
93
  * Execute gno models pull command.
88
94
  */
89
95
  export async function modelsPull(
90
- options: ModelsPullOptions = {}
96
+ options: ModelsPullOptions = {},
97
+ deps: ModelsPullDependencies = {}
91
98
  ): Promise<ModelsPullResult> {
92
99
  // Use provided config, or load from disk (use defaults if not initialized)
93
100
  let config = options.config;
@@ -98,7 +105,7 @@ export async function modelsPull(
98
105
  }
99
106
 
100
107
  const preset = getActivePreset(config);
101
- const cache = new ModelCache(getModelsCachePath());
108
+ const cache = deps.cache ?? new ModelCache(getModelsCachePath());
102
109
  const types = getTypesToPull(options);
103
110
 
104
111
  const results: ModelPullResult[] = [];
@@ -106,6 +113,7 @@ export async function modelsPull(
106
113
  let skipped = 0;
107
114
 
108
115
  for (const type of types) {
116
+ if (options.signal?.aborted) break;
109
117
  const uri =
110
118
  type === "expand" ? (preset.expand ?? preset.gen) : preset[type];
111
119
 
@@ -133,8 +141,10 @@ export async function modelsPull(
133
141
  (progress) => {
134
142
  options.onProgress?.(type, progress);
135
143
  },
136
- options.force
144
+ options.force,
145
+ options.signal
137
146
  );
147
+ if (options.signal?.aborted) break;
138
148
 
139
149
  if (result.ok) {
140
150
  results.push({
@@ -5,6 +5,8 @@
5
5
  * @module src/cli/commands/query
6
6
  */
7
7
 
8
+ import type { RetrievalTraceSurfaceMetadata } from "../../core/retrieval-trace-session";
9
+ import type { RetrievalTraceSession } from "../../core/retrieval-trace-session";
8
10
  import type {
9
11
  EmbeddingPort,
10
12
  GenerationPort,
@@ -16,6 +18,11 @@ import {
16
18
  fingerprintContentTypeRules,
17
19
  normalizeContentTypes,
18
20
  } from "../../config";
21
+ import {
22
+ finishRetrievalTraceAfterError,
23
+ retrievalTraceFilters,
24
+ startRetrievalTraceRequest,
25
+ } from "../../core/retrieval-trace-request";
19
26
  import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
20
27
  import { resolveDownloadPolicy } from "../../llm/policy";
21
28
  import { resolveModelUri } from "../../llm/registry";
@@ -72,7 +79,11 @@ export interface QueryFormatOptions {
72
79
  }
73
80
 
74
81
  export type QueryResult =
75
- | { success: true; data: SearchResults }
82
+ | {
83
+ success: true;
84
+ data: SearchResults;
85
+ metadata?: RetrievalTraceSurfaceMetadata;
86
+ }
76
87
  | { success: false; error: string };
77
88
 
78
89
  export type QueryDiagnoseCommandOptions = HybridSearchOptions & {
@@ -126,8 +137,47 @@ export async function query(
126
137
  let embedPort: EmbeddingPort | null = null;
127
138
  let expandPort: GenerationPort | null = null;
128
139
  let rerankPort: RerankPort | null = null;
140
+ let traceSession: RetrievalTraceSession | undefined;
129
141
 
130
142
  try {
143
+ const embedUri = resolveModelUri(
144
+ config,
145
+ "embed",
146
+ options.embedModel,
147
+ options.collection
148
+ );
149
+ const expandUri =
150
+ !options.noExpand && !options.queryModes?.length
151
+ ? resolveModelUri(
152
+ config,
153
+ "expand",
154
+ options.expandModel ?? options.genModel,
155
+ options.collection
156
+ )
157
+ : undefined;
158
+ const rerankUri = !options.noRerank
159
+ ? resolveModelUri(
160
+ config,
161
+ "rerank",
162
+ options.rerankModel,
163
+ options.collection
164
+ )
165
+ : undefined;
166
+ const traceStart = await startRetrievalTraceRequest({
167
+ store,
168
+ config,
169
+ query: queryText,
170
+ filters: retrievalTraceFilters({ ...options, limit }),
171
+ pipeline: "hybrid",
172
+ indexName: options.indexName,
173
+ modelUris: [embedUri, expandUri, rerankUri].filter(
174
+ (value): value is string => Boolean(value)
175
+ ),
176
+ });
177
+ if (!traceStart.ok) {
178
+ return { success: false, error: traceStart.error.message };
179
+ }
180
+ traceSession = traceStart.value ?? undefined;
131
181
  const llm = new LlmAdapter(config);
132
182
 
133
183
  // Resolve download policy from env/flags
@@ -143,12 +193,6 @@ export async function query(
143
193
  : undefined;
144
194
 
145
195
  // Create embedding port (for vector search)
146
- const embedUri = resolveModelUri(
147
- config,
148
- "embed",
149
- options.embedModel,
150
- options.collection
151
- );
152
196
  const embedResult = await llm.createEmbeddingPort(embedUri, {
153
197
  policy,
154
198
  onProgress: downloadProgress
@@ -161,13 +205,7 @@ export async function query(
161
205
 
162
206
  // Create expansion port - optional.
163
207
  // Skip when structured query modes are provided.
164
- if (!options.noExpand && !options.queryModes?.length) {
165
- const expandUri = resolveModelUri(
166
- config,
167
- "expand",
168
- options.expandModel ?? options.genModel,
169
- options.collection
170
- );
208
+ if (expandUri) {
171
209
  const genResult = await llm.createExpansionPort(expandUri, {
172
210
  policy,
173
211
  onProgress: downloadProgress
@@ -180,13 +218,7 @@ export async function query(
180
218
  }
181
219
 
182
220
  // Create rerank port - optional
183
- if (!options.noRerank) {
184
- const rerankUri = resolveModelUri(
185
- config,
186
- "rerank",
187
- options.rerankModel,
188
- options.collection
189
- );
221
+ if (rerankUri) {
190
222
  const rerankResult = await llm.createRerankPort(rerankUri, {
191
223
  policy,
192
224
  onProgress: downloadProgress
@@ -228,19 +260,27 @@ export async function query(
228
260
  expandPort,
229
261
  rerankPort,
230
262
  };
231
-
232
263
  const result = await searchHybrid(deps, queryText, {
233
264
  ...options,
234
265
  limit,
266
+ traceSession,
235
267
  });
236
268
 
237
269
  if (!result.ok) {
270
+ await traceSession?.finish("failed");
238
271
  return { success: false, error: result.error.message };
239
272
  }
240
273
 
241
274
  return {
242
275
  success: true,
243
276
  data: decorateSearchResultsForIndex(result.value, options.indexName),
277
+ metadata: traceSession?.metadata(),
278
+ };
279
+ } catch (cause) {
280
+ await finishRetrievalTraceAfterError(traceSession, cause);
281
+ return {
282
+ success: false,
283
+ error: cause instanceof Error ? cause.message : "Hybrid query failed",
244
284
  };
245
285
  } finally {
246
286
  if (embedPort) {
@@ -439,7 +479,6 @@ export function formatQuery(
439
479
  })
440
480
  : `Error: ${result.error}`;
441
481
  }
442
-
443
482
  // Output explain to stderr if present (async but best-effort)
444
483
  outputExplainToStderr(result.data);
445
484
 
@@ -0,0 +1,140 @@
1
+ /** Local read-only retrieval replay command. */
2
+
3
+ import type {
4
+ ReplayRetrievalTraceResult,
5
+ RetrievalReplayCandidate,
6
+ } from "../../core/retrieval-trace-management";
7
+ import type {
8
+ EmbeddingPort,
9
+ GenerationPort,
10
+ RerankPort,
11
+ } from "../../llm/types";
12
+ import type { VectorIndexPort } from "../../store/vector";
13
+
14
+ import { RetrievalTraceManagementService } from "../../core/retrieval-trace-management";
15
+ import { LlmAdapter } from "../../llm/nodeLlamaCpp/adapter";
16
+ import { resolveDownloadPolicy } from "../../llm/policy";
17
+ import { resolveModelUri } from "../../llm/registry";
18
+ import { createVectorIndexPort } from "../../store/vector";
19
+ import { CliError } from "../errors";
20
+ import { initStore } from "./shared";
21
+
22
+ export interface TraceReplayCommandOptions {
23
+ configPath?: string;
24
+ indexName?: string;
25
+ format?: "json" | "md";
26
+ offline?: boolean;
27
+ }
28
+
29
+ const unwrap = <T>(
30
+ value: { ok: true; value: T } | { ok: false; error: { message: string } }
31
+ ): T => {
32
+ if (value.ok) return value.value;
33
+ throw new CliError("RUNTIME", value.error.message);
34
+ };
35
+
36
+ const formatMarkdown = (value: ReplayRetrievalTraceResult): string => {
37
+ const lines = [
38
+ `# Retrieval replay \`${value.exportId}\``,
39
+ "",
40
+ `- Verdict: **${value.verdict}**`,
41
+ `- Recommendation: **${value.recommendation}**`,
42
+ `- Applied: **no**`,
43
+ ];
44
+ if (value.reason) lines.push(`- Reason: \`${value.reason}\``);
45
+ lines.push(
46
+ "",
47
+ "| Case | Status | Verdict | Baseline recall | Candidate recall |",
48
+ "|---|---|---|---:|---:|"
49
+ );
50
+ for (const item of value.cases) {
51
+ lines.push(
52
+ `| \`${item.caseId}\` | ${item.terminalStatus} | ${item.verdict} | ${item.metrics.baseline.recallAtK.toFixed(3)} | ${item.metrics.candidate.recallAtK.toFixed(3)} |`
53
+ );
54
+ }
55
+ return `${lines.join("\n")}\n`;
56
+ };
57
+
58
+ export const traceReplay = async (
59
+ exportId: string,
60
+ candidate: RetrievalReplayCandidate,
61
+ options: TraceReplayCommandOptions = {}
62
+ ): Promise<string> => {
63
+ const initialized = await initStore({
64
+ configPath: options.configPath,
65
+ indexName: options.indexName,
66
+ syncConfig: false,
67
+ allowEmptyCollections: true,
68
+ });
69
+ if (!initialized.ok) throw new CliError("RUNTIME", initialized.error);
70
+ const { config, store } = initialized;
71
+ let embedPort: EmbeddingPort | null = null;
72
+ let expandPort: GenerationPort | null = null;
73
+ let rerankPort: RerankPort | null = null;
74
+ let vectorIndex: VectorIndexPort | null = null;
75
+ try {
76
+ const needsSemantic = candidate.type !== "bm25";
77
+ const embedUri = needsSemantic
78
+ ? resolveModelUri(config, "embed")
79
+ : undefined;
80
+ const expandUri =
81
+ candidate.type === "hybrid" &&
82
+ !candidate.noExpand &&
83
+ !candidate.queryModes?.length
84
+ ? resolveModelUri(config, "expand")
85
+ : undefined;
86
+ const rerankUri =
87
+ candidate.type === "hybrid" && !candidate.noRerank
88
+ ? resolveModelUri(config, "rerank")
89
+ : undefined;
90
+ const llm = new LlmAdapter(config);
91
+ const policy = resolveDownloadPolicy(process.env, {
92
+ offline: options.offline,
93
+ });
94
+ if (embedUri) {
95
+ const created = await llm.createEmbeddingPort(embedUri, { policy });
96
+ if (created.ok) embedPort = created.value;
97
+ }
98
+ if (expandUri) {
99
+ const created = await llm.createExpansionPort(expandUri, { policy });
100
+ if (created.ok) expandPort = created.value;
101
+ }
102
+ if (rerankUri) {
103
+ const created = await llm.createRerankPort(rerankUri, { policy });
104
+ if (created.ok) rerankPort = created.value;
105
+ }
106
+ if (embedPort && embedUri) {
107
+ const initializedEmbed = await embedPort.init();
108
+ if (initializedEmbed.ok) {
109
+ const created = await createVectorIndexPort(store.getRawDb(), {
110
+ model: embedUri,
111
+ dimensions: embedPort.dimensions(),
112
+ });
113
+ if (created.ok) vectorIndex = created.value;
114
+ }
115
+ }
116
+ const replayed = await new RetrievalTraceManagementService(store).replay(
117
+ { exportId, candidate },
118
+ {
119
+ config,
120
+ vectorIndex,
121
+ embedPort,
122
+ expandPort,
123
+ rerankPort,
124
+ indexName: options.indexName,
125
+ modelUris: [embedUri, expandUri, rerankUri].filter(
126
+ (value): value is string => Boolean(value)
127
+ ),
128
+ }
129
+ );
130
+ const value = unwrap(replayed);
131
+ return options.format === "md"
132
+ ? formatMarkdown(value)
133
+ : `${JSON.stringify(value, null, 2)}\n`;
134
+ } finally {
135
+ await embedPort?.dispose();
136
+ await expandPort?.dispose();
137
+ await rerankPort?.dispose();
138
+ await store.close();
139
+ }
140
+ };
@@ -5,8 +5,16 @@
5
5
  * @module src/cli/commands/search
6
6
  */
7
7
 
8
+ import type {
9
+ RetrievalTraceSession,
10
+ RetrievalTraceSurfaceMetadata,
11
+ } from "../../core/retrieval-trace-session";
8
12
  import type { SearchOptions, SearchResults } from "../../pipeline/types";
9
13
 
14
+ import {
15
+ finishRetrievalTraceAfterError,
16
+ startRetrievalTraceRequest,
17
+ } from "../../core/retrieval-trace-request";
10
18
  import { searchBm25 } from "../../pipeline/search";
11
19
  import {
12
20
  type FormatOptions,
@@ -38,7 +46,11 @@ export type SearchCommandOptions = SearchOptions & {
38
46
  };
39
47
 
40
48
  export type SearchResult =
41
- | { success: true; data: SearchResults }
49
+ | {
50
+ success: true;
51
+ data: SearchResults;
52
+ metadata?: RetrievalTraceSurfaceMetadata;
53
+ }
42
54
  | { success: false; error: string; isValidation?: boolean };
43
55
 
44
56
  // ─────────────────────────────────────────────────────────────────────────────
@@ -68,15 +80,42 @@ export async function search(
68
80
  return { success: false, error: initResult.error };
69
81
  }
70
82
 
71
- const { store } = initResult;
83
+ const { store, config } = initResult;
84
+ let traceSession: RetrievalTraceSession | undefined;
72
85
 
73
86
  try {
87
+ const started = await startRetrievalTraceRequest({
88
+ store,
89
+ config,
90
+ query,
91
+ filters: {
92
+ limit,
93
+ collection: options.collection,
94
+ lang: options.lang,
95
+ full: options.full,
96
+ lineNumbers: options.lineNumbers,
97
+ tagsAll: options.tagsAll,
98
+ tagsAny: options.tagsAny,
99
+ since: options.since,
100
+ until: options.until,
101
+ categories: options.categories,
102
+ author: options.author,
103
+ intent: options.intent,
104
+ exclude: options.exclude,
105
+ },
106
+ pipeline: "bm25",
107
+ indexName: options.indexName,
108
+ });
109
+ if (!started.ok) return { success: false, error: started.error.message };
110
+ traceSession = started.value ?? undefined;
74
111
  const result = await searchBm25(store, query, {
75
112
  ...options,
76
113
  limit,
114
+ traceSession,
77
115
  });
78
116
 
79
117
  if (!result.ok) {
118
+ await traceSession?.finish("failed");
80
119
  // Map INVALID_INPUT to validation error for proper exit code
81
120
  const isValidation = result.error.code === "INVALID_INPUT";
82
121
  return {
@@ -89,6 +128,13 @@ export async function search(
89
128
  return {
90
129
  success: true,
91
130
  data: decorateSearchResultsForIndex(result.value, options.indexName),
131
+ metadata: traceSession?.metadata(),
132
+ };
133
+ } catch (cause) {
134
+ await finishRetrievalTraceAfterError(traceSession, cause);
135
+ return {
136
+ success: false,
137
+ error: cause instanceof Error ? cause.message : "Search failed",
92
138
  };
93
139
  } finally {
94
140
  await store.close();
@@ -135,7 +181,6 @@ export function formatSearch(
135
181
  })
136
182
  : `Error: ${result.error}`;
137
183
  }
138
-
139
184
  const formatOpts: FormatOptions = {
140
185
  format: getFormatType(options),
141
186
  full: options.full,
@@ -44,6 +44,8 @@ export interface InitStoreOptions {
44
44
  collection?: string;
45
45
  /** Sync collections/contexts from config into DB on open */
46
46
  syncConfig?: boolean;
47
+ /** Permit management commands that only need an existing index database */
48
+ allowEmptyCollections?: boolean;
47
49
  }
48
50
 
49
51
  /**
@@ -81,7 +83,7 @@ export async function initStore(
81
83
  }
82
84
  }
83
85
 
84
- if (collections.length === 0) {
86
+ if (collections.length === 0 && !options.allowEmptyCollections) {
85
87
  return {
86
88
  ok: false,
87
89
  error: "No collections configured. Run: gno collection add <path>",
@@ -15,6 +15,7 @@ import { buildActivationStatus } from "../../core/activation-status";
15
15
  import { ModelCache } from "../../llm/cache";
16
16
  import { getActivePreset, resolveModelUri } from "../../llm/registry";
17
17
  import { getConnectorVerificationTargets } from "../../serve/connectors";
18
+ import { createStandaloneResidentStatus } from "../../serve/resident-status";
18
19
  import { SqliteAdapter } from "../../store/sqlite/adapter";
19
20
 
20
21
  /**
@@ -277,6 +278,7 @@ export function formatStatus(
277
278
  const s = result.status;
278
279
  return JSON.stringify(
279
280
  {
281
+ resident: createStandaloneResidentStatus("direct-cli"),
280
282
  indexName: s.indexName,
281
283
  configPath: s.configPath,
282
284
  dbPath: s.dbPath,
@@ -0,0 +1,200 @@
1
+ /** Local retrieval-trace inspection, feedback, export, and deletion commands. */
2
+
3
+ import type {
4
+ RetrievalTraceDeleteResult,
5
+ RetrievalTraceDetail,
6
+ RetrievalTraceLabelRequest,
7
+ RetrievalTraceLabelResult,
8
+ RetrievalTraceListResult,
9
+ RetrievalTracePurgeResult,
10
+ } from "../../core/retrieval-trace-management";
11
+ import type { StoreResult } from "../../store/types";
12
+
13
+ import { atomicWrite } from "../../core/file-ops";
14
+ import { RetrievalTraceManagementService } from "../../core/retrieval-trace-management";
15
+ import { CliError } from "../errors";
16
+ import { initStore } from "./shared";
17
+
18
+ export type TraceOutputFormat = "json" | "md";
19
+
20
+ interface TraceCommandOptions {
21
+ configPath?: string;
22
+ indexName?: string;
23
+ format?: TraceOutputFormat;
24
+ }
25
+
26
+ interface TraceExportOptions extends TraceCommandOptions {
27
+ output?: string;
28
+ exportFormat?: "agentic-receipt" | "qrels";
29
+ }
30
+
31
+ const unwrapTraceResult = <T>(result: StoreResult<T>): T => {
32
+ if (result.ok) {
33
+ return result.value;
34
+ }
35
+ throw new CliError(
36
+ result.error.code === "INVALID_INPUT" ? "VALIDATION" : "RUNTIME",
37
+ result.error.message,
38
+ { details: { traceCode: result.error.code } }
39
+ );
40
+ };
41
+
42
+ const withTraceService = async <T>(
43
+ options: TraceCommandOptions,
44
+ operation: (service: RetrievalTraceManagementService) => Promise<T>
45
+ ): Promise<T> => {
46
+ const initialized = await initStore({
47
+ configPath: options.configPath,
48
+ indexName: options.indexName,
49
+ syncConfig: false,
50
+ allowEmptyCollections: true,
51
+ });
52
+ if (!initialized.ok) {
53
+ throw new CliError("RUNTIME", initialized.error);
54
+ }
55
+ try {
56
+ return await operation(
57
+ new RetrievalTraceManagementService(initialized.store)
58
+ );
59
+ } finally {
60
+ await initialized.store.close();
61
+ }
62
+ };
63
+
64
+ const json = (value: unknown): string => `${JSON.stringify(value, null, 2)}\n`;
65
+
66
+ const formatListMarkdown = (value: RetrievalTraceListResult): string => {
67
+ const lines = [
68
+ "# Retrieval traces",
69
+ "",
70
+ "| Trace | Status | Mode | Created |",
71
+ "|---|---|---|---|",
72
+ ];
73
+ for (const trace of value.traces) {
74
+ lines.push(
75
+ `| \`${trace.traceId}\` | ${trace.status} | ${trace.redactionMode} | ${new Date(trace.createdAtMs).toISOString()} |`
76
+ );
77
+ }
78
+ if (value.traces.length === 0) {
79
+ lines.push("| — | — | — | — |");
80
+ }
81
+ if (value.nextCursor) {
82
+ lines.push("", `Next cursor: \`${value.nextCursor}\``);
83
+ }
84
+ return `${lines.join("\n")}\n`;
85
+ };
86
+
87
+ const formatDetailMarkdown = (value: RetrievalTraceDetail): string => {
88
+ const trace = value.trace;
89
+ const lines = [
90
+ `# Retrieval trace \`${trace.traceId}\``,
91
+ "",
92
+ `- Status: **${trace.status}**`,
93
+ `- Redaction: **${trace.redactionMode}**`,
94
+ `- Replay capable: **${trace.replayCapable ? "yes" : "no"}**`,
95
+ `- Created: ${new Date(trace.createdAtMs).toISOString()}`,
96
+ `- Updated: ${new Date(trace.updatedAtMs).toISOString()}`,
97
+ "",
98
+ "## Records",
99
+ "",
100
+ `- Runs: ${value.runs.length}`,
101
+ `- Events: ${value.events.length}`,
102
+ `- Judgments: ${value.judgments.length}`,
103
+ `- Exports: ${value.exports.length}`,
104
+ ];
105
+ const truncatedSections = Object.entries(value.truncated)
106
+ .filter(([, truncated]) => truncated)
107
+ .map(([section]) => section);
108
+ if (truncatedSections.length > 0) {
109
+ lines.push(
110
+ "",
111
+ `Detail truncated: ${truncatedSections.join(", ")}. Request a larger bounded detail limit.`
112
+ );
113
+ }
114
+ return `${lines.join("\n")}\n`;
115
+ };
116
+
117
+ const formatMutationMarkdown = (
118
+ heading: string,
119
+ value:
120
+ | RetrievalTraceLabelResult
121
+ | RetrievalTraceDeleteResult
122
+ | RetrievalTracePurgeResult
123
+ ): string =>
124
+ `# ${heading}\n\n\`\`\`json\n${JSON.stringify(value, null, 2)}\n\`\`\`\n`;
125
+
126
+ export const traceList = async (
127
+ input: { limit?: number; cursor?: string },
128
+ options: TraceCommandOptions = {}
129
+ ): Promise<string> => {
130
+ const value = await withTraceService(options, async (service) =>
131
+ unwrapTraceResult(await service.list(input))
132
+ );
133
+ return options.format === "md" ? formatListMarkdown(value) : json(value);
134
+ };
135
+
136
+ export const traceShow = async (
137
+ traceId: string,
138
+ input: { detailLimit?: number },
139
+ options: TraceCommandOptions = {}
140
+ ): Promise<string> => {
141
+ const value = await withTraceService(options, async (service) =>
142
+ unwrapTraceResult(await service.show(traceId, input))
143
+ );
144
+ return options.format === "md" ? formatDetailMarkdown(value) : json(value);
145
+ };
146
+
147
+ export const traceLabel = async (
148
+ input: RetrievalTraceLabelRequest,
149
+ options: TraceCommandOptions = {}
150
+ ): Promise<string> => {
151
+ const value = await withTraceService(options, async (service) =>
152
+ unwrapTraceResult(await service.label(input))
153
+ );
154
+ return options.format === "md"
155
+ ? formatMutationMarkdown("Retrieval trace judgment", value)
156
+ : json(value);
157
+ };
158
+
159
+ export const traceExport = async (
160
+ traceIds: string[],
161
+ options: TraceExportOptions = {}
162
+ ): Promise<string> => {
163
+ const value = await withTraceService(options, async (service) =>
164
+ unwrapTraceResult(
165
+ await service.export({
166
+ traceIds,
167
+ format: options.exportFormat ?? "agentic-receipt",
168
+ })
169
+ )
170
+ );
171
+ const artifact = json(value.artifact);
172
+ if (options.output) {
173
+ await atomicWrite(options.output, artifact);
174
+ return "";
175
+ }
176
+ return json(value);
177
+ };
178
+
179
+ export const traceDelete = async (
180
+ traceId: string,
181
+ options: TraceCommandOptions = {}
182
+ ): Promise<string> => {
183
+ const value = await withTraceService(options, async (service) =>
184
+ unwrapTraceResult(await service.delete(traceId))
185
+ );
186
+ return options.format === "md"
187
+ ? formatMutationMarkdown("Retrieval trace deleted", value)
188
+ : json(value);
189
+ };
190
+
191
+ export const tracePurge = async (
192
+ options: TraceCommandOptions = {}
193
+ ): Promise<string> => {
194
+ const value = await withTraceService(options, async (service) =>
195
+ unwrapTraceResult(await service.purge())
196
+ );
197
+ return options.format === "md"
198
+ ? formatMutationMarkdown("Retrieval traces purged", value)
199
+ : json(value);
200
+ };