@gmickel/gno 1.19.0 → 1.21.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 +28 -8
- package/assets/skill/SKILL.md +73 -27
- package/assets/skill/mcp-reference.md +7 -2
- package/assets/skill/recipes/citation-and-provenance.md +32 -9
- package/package.json +1 -1
- package/spec/cli.md +142 -17
- package/spec/db/schema.sql +170 -0
- package/spec/evals-agentic.md +87 -5
- package/spec/mcp.md +75 -3
- package/spec/output-schemas/ask.schema.json +198 -0
- package/spec/output-schemas/capsule-reverified-event.schema.json +47 -0
- package/spec/output-schemas/changes.schema.json +280 -0
- package/spec/output-schemas/claim-verification.schema.json +291 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +36 -1
- package/spec/output-schemas/document-diff.schema.json +185 -0
- package/spec/output-schemas/impact.schema.json +122 -0
- package/spec/output-schemas/saved-capsule-list.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-registration.schema.json +172 -0
- package/spec/output-schemas/saved-capsule-reverification.schema.json +59 -0
- package/spec/output-schemas/saved-capsule-unwatch.schema.json +16 -0
- package/spec/output-schemas/saved-capsule-watch.schema.json +17 -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 +4 -0
- package/src/app/context-runtime.ts +5 -1
- 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 +40 -149
- package/src/cli/commands/changes.ts +160 -0
- package/src/cli/commands/context-saved.ts +189 -0
- package/src/cli/options.ts +8 -0
- package/src/cli/program.ts +227 -1
- package/src/core/capsule-registry.ts +279 -0
- package/src/core/capsule-reverification-scheduler.ts +218 -0
- package/src/core/capsule-reverification.ts +289 -0
- package/src/core/change-diff.ts +182 -0
- package/src/core/change-journal.ts +228 -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 +33 -21
- package/src/core/context-evidence.ts +6 -0
- package/src/core/knowledge-delta.ts +395 -0
- package/src/core/knowledge-impact.ts +202 -0
- package/src/core/retrieval-trace-evidence-origin.ts +3 -0
- package/src/core/retrieval-trace-session.ts +15 -2
- package/src/ingestion/sync.ts +214 -165
- 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/changes.ts +80 -0
- package/src/mcp/tools/context.ts +28 -7
- package/src/mcp/tools/index.ts +38 -0
- 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/types.ts +25 -0
- package/src/sdk/client.ts +77 -2
- package/src/sdk/index.ts +7 -0
- package/src/sdk/types.ts +22 -0
- package/src/serve/doc-events.ts +12 -1
- package/src/serve/public/components/AskVerificationPanel.tsx +189 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Ask.tsx +42 -4
- package/src/serve/resident-runtime.ts +22 -0
- package/src/serve/routes/api.ts +162 -3
- package/src/serve/routes/changes.ts +102 -0
- package/src/serve/server.ts +34 -0
- package/src/serve/watch-service.ts +9 -0
- package/src/store/index.ts +21 -0
- package/src/store/migrations/015-document-change-journal.ts +85 -0
- package/src/store/migrations/016-saved-capsules.ts +131 -0
- package/src/store/migrations/017-document-change-retention-counters.ts +33 -0
- package/src/store/migrations/018-saved-capsule-registration-epoch.ts +24 -0
- package/src/store/migrations/019-saved-capsule-registration-generation.ts +53 -0
- package/src/store/migrations/index.ts +10 -0
- package/src/store/sqlite/adapter.ts +291 -7
- package/src/store/sqlite/capsule-registry-store.ts +534 -0
- package/src/store/sqlite/change-journal-store.ts +473 -0
- package/src/store/types.ts +262 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/** MCP verified Ask tool over the shared closed-evidence application boundary. */
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import type { RetrievalTraceSession } from "../../core/retrieval-trace-session";
|
|
6
|
+
import type { QueryModeInput } from "../../pipeline/types";
|
|
7
|
+
import type { ToolContext } from "../server";
|
|
8
|
+
import type { ToolResult } from "./index";
|
|
9
|
+
|
|
10
|
+
import { buildVerifiedAsk } from "../../app/verified-ask";
|
|
11
|
+
import {
|
|
12
|
+
finishRetrievalTraceAfterError,
|
|
13
|
+
retrievalTraceFilters,
|
|
14
|
+
startRetrievalTraceRequest,
|
|
15
|
+
} from "../../core/retrieval-trace-request";
|
|
16
|
+
import { attachRetrievalTraceMetadata } from "../../core/retrieval-trace-session";
|
|
17
|
+
import { normalizeStructuredQueryInput } from "../../core/structured-query";
|
|
18
|
+
import { resolveModelUri } from "../../llm/registry";
|
|
19
|
+
import { answerTraceTerminalStatus } from "../../pipeline/answer";
|
|
20
|
+
import { createMcpModelPorts, type McpModelPortFactory } from "./context";
|
|
21
|
+
import { normalizeTagFilters, runTool } from "./index";
|
|
22
|
+
|
|
23
|
+
const queryModeSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
mode: z.enum(["term", "intent", "hyde"]),
|
|
26
|
+
text: z.string().trim().min(1),
|
|
27
|
+
})
|
|
28
|
+
.strict();
|
|
29
|
+
|
|
30
|
+
export const askInputSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
query: z.string().trim().min(1),
|
|
33
|
+
verify: z.literal(true),
|
|
34
|
+
collection: z.string().optional(),
|
|
35
|
+
limit: z.number().int().min(1).max(100).default(5),
|
|
36
|
+
minScore: z.number().min(0).max(1).optional(),
|
|
37
|
+
lang: z.string().optional(),
|
|
38
|
+
intent: z.string().optional(),
|
|
39
|
+
candidateLimit: z.number().int().min(1).max(100).optional(),
|
|
40
|
+
exclude: z.array(z.string()).optional(),
|
|
41
|
+
queryModes: z.array(queryModeSchema).optional(),
|
|
42
|
+
tagsAll: z.array(z.string()).optional(),
|
|
43
|
+
tagsAny: z.array(z.string()).optional(),
|
|
44
|
+
since: z.string().optional(),
|
|
45
|
+
until: z.string().optional(),
|
|
46
|
+
categories: z.array(z.string()).optional(),
|
|
47
|
+
author: z.string().optional(),
|
|
48
|
+
graph: z.boolean().optional(),
|
|
49
|
+
noGraph: z.boolean().optional(),
|
|
50
|
+
noRerank: z.boolean().optional(),
|
|
51
|
+
maxAnswerTokens: z.number().int().positive().optional(),
|
|
52
|
+
contextBudgetTokens: z.number().int().positive().optional(),
|
|
53
|
+
contextBudgetBytes: z.number().int().positive().optional(),
|
|
54
|
+
})
|
|
55
|
+
.strict();
|
|
56
|
+
|
|
57
|
+
type AskInput = z.infer<typeof askInputSchema>;
|
|
58
|
+
|
|
59
|
+
export interface HandleAskDependencies {
|
|
60
|
+
modelPortFactory?: McpModelPortFactory;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const exactSpan = (uri: string, startLine: number, endLine: number): string =>
|
|
64
|
+
`${uri}:L${startLine}${startLine === endLine ? "" : `-L${endLine}`}`;
|
|
65
|
+
|
|
66
|
+
export const formatVerifiedAskReadable = (
|
|
67
|
+
result: Awaited<ReturnType<typeof buildVerifiedAsk>>
|
|
68
|
+
): string => {
|
|
69
|
+
const numbers = new Map(
|
|
70
|
+
(result.citations ?? []).flatMap((citation, index) =>
|
|
71
|
+
citation.evidenceId ? [[citation.evidenceId, index + 1] as const] : []
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
const answer = (result.answer ?? "").replace(
|
|
75
|
+
/\[evidence:([a-f0-9]{64})\]/g,
|
|
76
|
+
(_marker, evidenceId: string) => {
|
|
77
|
+
const number = numbers.get(evidenceId);
|
|
78
|
+
return number === undefined ? "" : `[${number}]`;
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
const claims = result.verification?.claims;
|
|
82
|
+
const lines = [
|
|
83
|
+
answer || "No verified answer.",
|
|
84
|
+
"",
|
|
85
|
+
`Verification: ${claims?.answerStatus ?? "unavailable"}`,
|
|
86
|
+
];
|
|
87
|
+
if (claims?.abstentionReason) {
|
|
88
|
+
lines.push(`Reason: ${claims.abstentionReason}`);
|
|
89
|
+
}
|
|
90
|
+
if (claims) {
|
|
91
|
+
lines.push(
|
|
92
|
+
`Claims: ${claims.coverage.supportedClaims}/${claims.coverage.totalClaims} supported`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const semantic = result.verification?.semantic;
|
|
96
|
+
if (semantic) {
|
|
97
|
+
lines.push(`Semantic verifier: ${semantic.status} (${semantic.reason})`);
|
|
98
|
+
}
|
|
99
|
+
for (const claim of claims?.claims ?? []) {
|
|
100
|
+
lines.push(`- ${claim.status}: ${claim.text}`);
|
|
101
|
+
for (const evidence of claim.evidence) {
|
|
102
|
+
lines.push(
|
|
103
|
+
` ${exactSpan(evidence.uri, evidence.startLine, evidence.endLine)} (${evidence.evidenceId})`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
for (const gap of result.verification?.capsule.coverage.gaps ?? []) {
|
|
108
|
+
lines.push(`Gap: ${gap.facet} (${gap.code})`);
|
|
109
|
+
}
|
|
110
|
+
for (const facet of result.verification?.capsule.coverage.unresolvedFacets ??
|
|
111
|
+
[]) {
|
|
112
|
+
lines.push(`Unresolved facet: ${facet}`);
|
|
113
|
+
}
|
|
114
|
+
for (const [name, state] of Object.entries(
|
|
115
|
+
result.verification?.capsule.retrieval.capabilityStates ?? {}
|
|
116
|
+
)) {
|
|
117
|
+
if (state.requested && state.outcome !== "used") {
|
|
118
|
+
lines.push(
|
|
119
|
+
`Capability: ${name} ${state.outcome}${state.fallbackReasons.length > 0 ? ` (${state.fallbackReasons.join(", ")})` : ""}`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const [index, citation] of (result.citations ?? []).entries()) {
|
|
124
|
+
const span =
|
|
125
|
+
citation.startLine === undefined || citation.endLine === undefined
|
|
126
|
+
? citation.uri
|
|
127
|
+
: exactSpan(citation.uri, citation.startLine, citation.endLine);
|
|
128
|
+
lines.push(`[${index + 1}] ${span} (${citation.evidenceId ?? "unknown"})`);
|
|
129
|
+
}
|
|
130
|
+
return lines.join("\n");
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const handleAsk = (
|
|
134
|
+
args: AskInput,
|
|
135
|
+
context: ToolContext,
|
|
136
|
+
dependencies: HandleAskDependencies = {}
|
|
137
|
+
): Promise<ToolResult> =>
|
|
138
|
+
runTool(
|
|
139
|
+
context,
|
|
140
|
+
"gno_ask",
|
|
141
|
+
async () => {
|
|
142
|
+
if (
|
|
143
|
+
args.collection &&
|
|
144
|
+
!context.collections.some(
|
|
145
|
+
(collection) => collection.name === args.collection
|
|
146
|
+
)
|
|
147
|
+
) {
|
|
148
|
+
throw new Error(`Collection not found: ${args.collection}`);
|
|
149
|
+
}
|
|
150
|
+
const normalized = normalizeStructuredQueryInput(
|
|
151
|
+
args.query,
|
|
152
|
+
(args.queryModes ?? []) as QueryModeInput[]
|
|
153
|
+
);
|
|
154
|
+
if (!normalized.ok) throw new Error(normalized.error.message);
|
|
155
|
+
const query = normalized.value.query;
|
|
156
|
+
const options = {
|
|
157
|
+
...args,
|
|
158
|
+
queryModes:
|
|
159
|
+
normalized.value.queryModes.length > 0
|
|
160
|
+
? normalized.value.queryModes
|
|
161
|
+
: undefined,
|
|
162
|
+
tagsAll: normalizeTagFilters(args.tagsAll),
|
|
163
|
+
tagsAny: normalizeTagFilters(args.tagsAny),
|
|
164
|
+
};
|
|
165
|
+
let traceSession: RetrievalTraceSession | undefined;
|
|
166
|
+
let modelPorts: Awaited<ReturnType<typeof createMcpModelPorts>> | null =
|
|
167
|
+
null;
|
|
168
|
+
try {
|
|
169
|
+
const started = await startRetrievalTraceRequest({
|
|
170
|
+
store: context.store,
|
|
171
|
+
config: context.config,
|
|
172
|
+
query,
|
|
173
|
+
filters: retrievalTraceFilters(options),
|
|
174
|
+
pipeline: "ask",
|
|
175
|
+
indexName: context.indexName,
|
|
176
|
+
modelUris: [
|
|
177
|
+
resolveModelUri(
|
|
178
|
+
context.config,
|
|
179
|
+
"embed",
|
|
180
|
+
undefined,
|
|
181
|
+
args.collection
|
|
182
|
+
),
|
|
183
|
+
resolveModelUri(
|
|
184
|
+
context.config,
|
|
185
|
+
"rerank",
|
|
186
|
+
undefined,
|
|
187
|
+
args.collection
|
|
188
|
+
),
|
|
189
|
+
resolveModelUri(context.config, "gen", undefined, args.collection),
|
|
190
|
+
],
|
|
191
|
+
});
|
|
192
|
+
if (!started.ok) throw new Error(started.error.message);
|
|
193
|
+
traceSession = started.value ?? undefined;
|
|
194
|
+
modelPorts = await createMcpModelPorts(
|
|
195
|
+
context,
|
|
196
|
+
args.collection,
|
|
197
|
+
dependencies.modelPortFactory,
|
|
198
|
+
{ generation: true }
|
|
199
|
+
);
|
|
200
|
+
if (!modelPorts.genPort) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
"Answer generation requested but no generation model is available"
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
const result = await buildVerifiedAsk(query, options, {
|
|
206
|
+
store: context.store,
|
|
207
|
+
config: context.config,
|
|
208
|
+
indexName: context.indexName,
|
|
209
|
+
vectorIndex: modelPorts.vectorIndex,
|
|
210
|
+
embedPort: modelPorts.embedPort,
|
|
211
|
+
rerankPort: modelPorts.rerankPort,
|
|
212
|
+
genPort: modelPorts.genPort,
|
|
213
|
+
traceSession,
|
|
214
|
+
});
|
|
215
|
+
const finished = await traceSession?.finish(
|
|
216
|
+
answerTraceTerminalStatus(result.citations)
|
|
217
|
+
);
|
|
218
|
+
if (finished && !finished.ok) throw new Error(finished.error.message);
|
|
219
|
+
return attachRetrievalTraceMetadata(result, traceSession);
|
|
220
|
+
} catch (error) {
|
|
221
|
+
await finishRetrievalTraceAfterError(traceSession, error);
|
|
222
|
+
throw error;
|
|
223
|
+
} finally {
|
|
224
|
+
await modelPorts?.dispose();
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
formatVerifiedAskReadable
|
|
228
|
+
);
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Read-only MCP adapters for knowledge change, diff, and impact services. */
|
|
2
|
+
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
import type { ToolContext } from "../server";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
analyzeKnowledgeImpact,
|
|
9
|
+
getKnowledgeDiff,
|
|
10
|
+
listKnowledgeChanges,
|
|
11
|
+
} from "../../core/knowledge-delta";
|
|
12
|
+
import { runTool, type ToolResult } from "./index";
|
|
13
|
+
|
|
14
|
+
export const changesInputSchema = z.object({
|
|
15
|
+
since: z.string().trim().min(1).max(512).optional(),
|
|
16
|
+
collection: z.string().trim().min(1).max(256).optional(),
|
|
17
|
+
limit: z.number().int().min(1).max(1000).default(100),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const diffInputSchema = z.object({
|
|
21
|
+
ref: z.string().trim().min(1).max(4096),
|
|
22
|
+
change: z.string().trim().min(1).max(512).optional(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
export const impactInputSchema = z.object({
|
|
26
|
+
ref: z.string().trim().min(1).max(4096),
|
|
27
|
+
maxDepth: z.number().int().min(1).max(6).default(3),
|
|
28
|
+
maxNodes: z.number().int().min(1).max(1000).default(100),
|
|
29
|
+
maxEdges: z.number().int().min(1).max(5000).default(250),
|
|
30
|
+
frontierLimit: z.number().int().min(1).max(1000).default(100),
|
|
31
|
+
visitedLimit: z.number().int().min(1).max(5000).default(500),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const unwrap = <T>(
|
|
35
|
+
result:
|
|
36
|
+
| { success: true; data: T }
|
|
37
|
+
| { success: false; error: string; isValidation?: boolean }
|
|
38
|
+
): T => {
|
|
39
|
+
if (result.success) return result.data;
|
|
40
|
+
throw new Error(
|
|
41
|
+
`${result.isValidation ? "VALIDATION" : "RUNTIME"}: ${result.error}`
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const handleChanges = (
|
|
46
|
+
args: z.infer<typeof changesInputSchema>,
|
|
47
|
+
ctx: ToolContext
|
|
48
|
+
): Promise<ToolResult> =>
|
|
49
|
+
runTool(
|
|
50
|
+
ctx,
|
|
51
|
+
"gno_changes",
|
|
52
|
+
async () => unwrap(await listKnowledgeChanges(ctx.store, args)),
|
|
53
|
+
(data) =>
|
|
54
|
+
`${data.changes.length} retained document changes${data.page.truncated ? " (more available)" : ""}`
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
export const handleDiff = (
|
|
58
|
+
args: z.infer<typeof diffInputSchema>,
|
|
59
|
+
ctx: ToolContext
|
|
60
|
+
): Promise<ToolResult> =>
|
|
61
|
+
runTool(
|
|
62
|
+
ctx,
|
|
63
|
+
"gno_diff",
|
|
64
|
+
async () =>
|
|
65
|
+
unwrap(await getKnowledgeDiff(ctx.store, args.ref, args.change)),
|
|
66
|
+
(data) =>
|
|
67
|
+
`Structural diff for ${data.document.uri}: ${data.status}; history ${data.history.status}; source bodies not retained`
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
export const handleImpact = (
|
|
71
|
+
args: z.infer<typeof impactInputSchema>,
|
|
72
|
+
ctx: ToolContext
|
|
73
|
+
): Promise<ToolResult> =>
|
|
74
|
+
runTool(
|
|
75
|
+
ctx,
|
|
76
|
+
"gno_impact",
|
|
77
|
+
async () => unwrap(await analyzeKnowledgeImpact(ctx.store, args.ref, args)),
|
|
78
|
+
(data) =>
|
|
79
|
+
`${data.impacted.length} documents depend on ${data.root.uri}${data.meta.truncated ? " (truncated)" : ""}`
|
|
80
|
+
);
|
package/src/mcp/tools/context.ts
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import type { RetrievalTraceSession } from "../../core/retrieval-trace-session";
|
|
4
4
|
import type { ModelLease } from "../../llm/nodeLlamaCpp/lifecycle";
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
EmbeddingPort,
|
|
7
|
+
GenerationPort,
|
|
8
|
+
RerankPort,
|
|
9
|
+
} from "../../llm/types";
|
|
6
10
|
import type { VectorIndexPort } from "../../store/vector";
|
|
7
11
|
import type { ToolContext } from "../server";
|
|
8
12
|
import type { ToolResult } from "./index";
|
|
@@ -85,13 +89,15 @@ const runContextTool = async (
|
|
|
85
89
|
|
|
86
90
|
interface McpModelPorts {
|
|
87
91
|
embedPort: EmbeddingPort | null;
|
|
92
|
+
genPort: GenerationPort | null;
|
|
88
93
|
rerankPort: RerankPort | null;
|
|
89
94
|
vectorIndex: VectorIndexPort | null;
|
|
90
95
|
dispose(): Promise<void>;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
|
-
interface McpModelPortFactory {
|
|
98
|
+
export interface McpModelPortFactory {
|
|
94
99
|
createEmbeddingPort: LlmAdapter["createEmbeddingPort"];
|
|
100
|
+
createGenerationPort?: LlmAdapter["createGenerationPort"];
|
|
95
101
|
createRerankPort: LlmAdapter["createRerankPort"];
|
|
96
102
|
acquireModelLease?: LlmAdapter["acquireModelLease"];
|
|
97
103
|
}
|
|
@@ -109,7 +115,8 @@ export const disposeContextModelOwners = async (
|
|
|
109
115
|
export const createMcpModelPorts = async (
|
|
110
116
|
context: ToolContext,
|
|
111
117
|
collection?: string,
|
|
112
|
-
factoryOverride?: McpModelPortFactory
|
|
118
|
+
factoryOverride?: McpModelPortFactory,
|
|
119
|
+
options: { generation?: boolean } = {}
|
|
113
120
|
): Promise<McpModelPorts> => {
|
|
114
121
|
const llm = new LlmAdapter(context.config);
|
|
115
122
|
const factory = factoryOverride ?? llm;
|
|
@@ -125,6 +132,7 @@ export const createMcpModelPorts = async (
|
|
|
125
132
|
let embedPort: EmbeddingPort | null = null;
|
|
126
133
|
let ownedEmbedPort: EmbeddingPort | null = null;
|
|
127
134
|
let rerankPort: RerankPort | null = null;
|
|
135
|
+
let genPort: GenerationPort | null = null;
|
|
128
136
|
let vectorIndex: VectorIndexPort | null = null;
|
|
129
137
|
try {
|
|
130
138
|
const embedResult = await factory.createEmbeddingPort(embedUri, {
|
|
@@ -152,14 +160,26 @@ export const createMcpModelPorts = async (
|
|
|
152
160
|
}
|
|
153
161
|
);
|
|
154
162
|
if (rerankResult.ok) rerankPort = rerankResult.value;
|
|
163
|
+
if (options.generation && factory.createGenerationPort) {
|
|
164
|
+
const genResult = await factory.createGenerationPort(
|
|
165
|
+
resolveModelUri(context.config, "gen", undefined, collection),
|
|
166
|
+
{
|
|
167
|
+
policy,
|
|
168
|
+
onProgress: (value) => progress("gen", value),
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
if (genResult.ok) genPort = genResult.value;
|
|
172
|
+
}
|
|
155
173
|
return {
|
|
156
174
|
embedPort,
|
|
175
|
+
genPort,
|
|
157
176
|
rerankPort,
|
|
158
177
|
vectorIndex,
|
|
159
178
|
async dispose() {
|
|
160
179
|
await disposeContextModelOwners(
|
|
161
|
-
[ownedEmbedPort, rerankPort].filter(
|
|
162
|
-
(port): port is EmbeddingPort | RerankPort
|
|
180
|
+
[ownedEmbedPort, rerankPort, genPort].filter(
|
|
181
|
+
(port): port is EmbeddingPort | RerankPort | GenerationPort =>
|
|
182
|
+
port !== null
|
|
163
183
|
),
|
|
164
184
|
lease
|
|
165
185
|
);
|
|
@@ -167,8 +187,9 @@ export const createMcpModelPorts = async (
|
|
|
167
187
|
};
|
|
168
188
|
} catch (error) {
|
|
169
189
|
await disposeContextModelOwners(
|
|
170
|
-
[ownedEmbedPort, rerankPort].filter(
|
|
171
|
-
(port): port is EmbeddingPort | RerankPort
|
|
190
|
+
[ownedEmbedPort, rerankPort, genPort].filter(
|
|
191
|
+
(port): port is EmbeddingPort | RerankPort | GenerationPort =>
|
|
192
|
+
port !== null
|
|
172
193
|
),
|
|
173
194
|
lease
|
|
174
195
|
);
|
package/src/mcp/tools/index.ts
CHANGED
|
@@ -19,7 +19,16 @@ import { NOTE_PRESETS, type NotePresetId } from "../../core/note-presets";
|
|
|
19
19
|
import { RETRIEVAL_TRACE_METADATA } from "../../core/retrieval-trace-session";
|
|
20
20
|
import { normalizeTag } from "../../core/tags";
|
|
21
21
|
import { handleAddCollection } from "./add-collection";
|
|
22
|
+
import { askInputSchema, handleAsk } from "./ask";
|
|
22
23
|
import { handleCapture } from "./capture";
|
|
24
|
+
import {
|
|
25
|
+
changesInputSchema,
|
|
26
|
+
diffInputSchema,
|
|
27
|
+
handleChanges,
|
|
28
|
+
handleDiff,
|
|
29
|
+
handleImpact,
|
|
30
|
+
impactInputSchema,
|
|
31
|
+
} from "./changes";
|
|
23
32
|
import { handleClearCollectionEmbeddings } from "./clear-collection-embeddings";
|
|
24
33
|
import { handleContext, handleContextVerify } from "./context";
|
|
25
34
|
import { handleEmbed } from "./embed";
|
|
@@ -96,6 +105,7 @@ export const MCP_TOOL_DESCRIPTIONS = {
|
|
|
96
105
|
"Compile one deterministic, budgeted, extractive evidence Capsule with exact line spans, coverage gaps, omissions, provenance, and verification fingerprints. Raw search/get tools remain available for manual retrieval.",
|
|
97
106
|
contextVerify:
|
|
98
107
|
"Verify a saved Context Capsule without rebuilding or mutating it. Reports unchanged, stale, missing, reranked, and fingerprint drift states against the active index.",
|
|
108
|
+
ask: "Generate one answer from a deterministic Context Capsule, verify every substantive claim against exact retained spans, and abstain unless support coverage is complete. Read-only; returns the Capsule, freshness receipt, claim verdicts, gaps, and evidence IDs.",
|
|
99
109
|
} as const;
|
|
100
110
|
|
|
101
111
|
/** Tool names whose execution mutates disk, config, or index state. */
|
|
@@ -978,6 +988,13 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
|
|
|
978
988
|
(args) => handleContextVerify(args, ctx)
|
|
979
989
|
);
|
|
980
990
|
|
|
991
|
+
server.tool(
|
|
992
|
+
"gno_ask",
|
|
993
|
+
MCP_TOOL_DESCRIPTIONS.ask,
|
|
994
|
+
askInputSchema.shape,
|
|
995
|
+
(args) => handleAsk(args, ctx)
|
|
996
|
+
);
|
|
997
|
+
|
|
981
998
|
server.tool(
|
|
982
999
|
"gno_search",
|
|
983
1000
|
MCP_TOOL_DESCRIPTIONS.search,
|
|
@@ -1027,6 +1044,27 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
|
|
|
1027
1044
|
(args) => handleStatus(args, ctx)
|
|
1028
1045
|
);
|
|
1029
1046
|
|
|
1047
|
+
server.tool(
|
|
1048
|
+
"gno_changes",
|
|
1049
|
+
"List retained metadata-only document changes with opaque cursor pagination and retention disclosure.",
|
|
1050
|
+
changesInputSchema.shape,
|
|
1051
|
+
(args) => handleChanges(args, ctx)
|
|
1052
|
+
);
|
|
1053
|
+
|
|
1054
|
+
server.tool(
|
|
1055
|
+
"gno_diff",
|
|
1056
|
+
"Inspect one retained metadata-only structural document change. Source bodies are never returned.",
|
|
1057
|
+
diffInputSchema.shape,
|
|
1058
|
+
(args) => handleDiff(args, ctx)
|
|
1059
|
+
);
|
|
1060
|
+
|
|
1061
|
+
server.tool(
|
|
1062
|
+
"gno_impact",
|
|
1063
|
+
"Find bounded inbound typed, wiki, and Markdown dependencies with deterministic evidence paths.",
|
|
1064
|
+
impactInputSchema.shape,
|
|
1065
|
+
(args) => handleImpact(args, ctx)
|
|
1066
|
+
);
|
|
1067
|
+
|
|
1030
1068
|
server.tool(
|
|
1031
1069
|
"gno_trace_list",
|
|
1032
1070
|
"List bounded metadata-only summaries of private local retrieval traces. Raw replay queries are omitted from history.",
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const CLAIM_VERIFICATION_SCHEMA_VERSION = "1.0" as const;
|
|
4
|
+
export const CLAIM_COORDINATE_SPACE = "utf16_code_units" as const;
|
|
5
|
+
|
|
6
|
+
const sha256Schema = z.string().regex(/^[a-f0-9]{64}$/);
|
|
7
|
+
|
|
8
|
+
export const semanticClaimJudgmentSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
claimId: sha256Schema,
|
|
11
|
+
verdict: z.enum(["supported", "contradicted"]),
|
|
12
|
+
confidence: z.number().min(0).max(1),
|
|
13
|
+
evidenceIds: z.array(sha256Schema).min(1).max(256),
|
|
14
|
+
rationaleCode: z.enum(["semantic_entailment", "semantic_contradiction"]),
|
|
15
|
+
verifierFingerprint: sha256Schema,
|
|
16
|
+
})
|
|
17
|
+
.strict()
|
|
18
|
+
.superRefine((value, context) => {
|
|
19
|
+
const expected =
|
|
20
|
+
value.verdict === "supported"
|
|
21
|
+
? "semantic_entailment"
|
|
22
|
+
: "semantic_contradiction";
|
|
23
|
+
if (
|
|
24
|
+
value.rationaleCode !== expected ||
|
|
25
|
+
new Set(value.evidenceIds).size !== value.evidenceIds.length
|
|
26
|
+
) {
|
|
27
|
+
context.addIssue({
|
|
28
|
+
code: "custom",
|
|
29
|
+
message: "semantic judgment is incoherent",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export type SemanticClaimJudgment = z.infer<typeof semanticClaimJudgmentSchema>;
|
|
35
|
+
const evidenceReferenceSchema = z
|
|
36
|
+
.object({
|
|
37
|
+
evidenceId: sha256Schema,
|
|
38
|
+
uri: z.string().min(1).max(2048).startsWith("gno://"),
|
|
39
|
+
startLine: z.number().int().positive(),
|
|
40
|
+
endLine: z.number().int().positive(),
|
|
41
|
+
text: z.string().min(1),
|
|
42
|
+
sourceHash: sha256Schema,
|
|
43
|
+
mirrorHash: sha256Schema,
|
|
44
|
+
passageHash: sha256Schema,
|
|
45
|
+
})
|
|
46
|
+
.strict()
|
|
47
|
+
.refine((value) => value.endLine >= value.startLine, {
|
|
48
|
+
message: "evidence line range is reversed",
|
|
49
|
+
path: ["endLine"],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const rejectedCitationSchema = z
|
|
53
|
+
.object({
|
|
54
|
+
marker: z.string().min(1),
|
|
55
|
+
start: z.number().int().nonnegative(),
|
|
56
|
+
end: z.number().int().positive(),
|
|
57
|
+
evidenceId: sha256Schema.nullable(),
|
|
58
|
+
reason: z.enum([
|
|
59
|
+
"malformed_citation",
|
|
60
|
+
"out_of_capsule",
|
|
61
|
+
"freshness_unavailable",
|
|
62
|
+
"freshness_receipt_invalid",
|
|
63
|
+
"freshness_receipt_mismatch",
|
|
64
|
+
"evidence_stale",
|
|
65
|
+
"evidence_missing",
|
|
66
|
+
"orphan_citation",
|
|
67
|
+
]),
|
|
68
|
+
})
|
|
69
|
+
.strict()
|
|
70
|
+
.refine((value) => value.end > value.start, {
|
|
71
|
+
message: "citation span must be non-empty",
|
|
72
|
+
path: ["end"],
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const verifiedClaimSchema = z
|
|
76
|
+
.object({
|
|
77
|
+
claimId: sha256Schema,
|
|
78
|
+
text: z.string().min(1),
|
|
79
|
+
start: z.number().int().nonnegative(),
|
|
80
|
+
end: z.number().int().positive(),
|
|
81
|
+
status: z.enum(["supported", "contradicted", "insufficient", "uncertain"]),
|
|
82
|
+
confidence: z.number().min(0).max(1).nullable(),
|
|
83
|
+
rationaleCode: z.enum([
|
|
84
|
+
"semantic_entailment",
|
|
85
|
+
"semantic_contradiction",
|
|
86
|
+
"no_valid_evidence",
|
|
87
|
+
"semantic_judgment_unavailable",
|
|
88
|
+
]),
|
|
89
|
+
verifierFingerprint: sha256Schema.nullable(),
|
|
90
|
+
evidence: z.array(evidenceReferenceSchema).max(256),
|
|
91
|
+
rejectedCitations: z.array(rejectedCitationSchema).max(256),
|
|
92
|
+
})
|
|
93
|
+
.strict()
|
|
94
|
+
.superRefine((value, context) => {
|
|
95
|
+
if (value.end <= value.start) {
|
|
96
|
+
context.addIssue({
|
|
97
|
+
code: "custom",
|
|
98
|
+
message: "claim span must be non-empty",
|
|
99
|
+
path: ["end"],
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const semantic =
|
|
103
|
+
value.status === "supported" || value.status === "contradicted";
|
|
104
|
+
const expectedRationale = {
|
|
105
|
+
supported: "semantic_entailment",
|
|
106
|
+
contradicted: "semantic_contradiction",
|
|
107
|
+
insufficient: "no_valid_evidence",
|
|
108
|
+
uncertain: "semantic_judgment_unavailable",
|
|
109
|
+
}[value.status];
|
|
110
|
+
const evidenceCountValid =
|
|
111
|
+
value.status === "insufficient"
|
|
112
|
+
? value.evidence.length === 0
|
|
113
|
+
: value.evidence.length > 0;
|
|
114
|
+
if (
|
|
115
|
+
value.rationaleCode !== expectedRationale ||
|
|
116
|
+
!evidenceCountValid ||
|
|
117
|
+
(semantic &&
|
|
118
|
+
(value.confidence === null || value.verifierFingerprint === null)) ||
|
|
119
|
+
(!semantic &&
|
|
120
|
+
(value.confidence !== null || value.verifierFingerprint !== null))
|
|
121
|
+
) {
|
|
122
|
+
context.addIssue({
|
|
123
|
+
code: "custom",
|
|
124
|
+
message: "claim status fields are incoherent",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
const ids = value.evidence.map((item) => item.evidenceId);
|
|
128
|
+
if (new Set(ids).size !== ids.length) {
|
|
129
|
+
context.addIssue({
|
|
130
|
+
code: "custom",
|
|
131
|
+
message: "claim evidence must be unique",
|
|
132
|
+
path: ["evidence"],
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const coverageSchema = z
|
|
138
|
+
.object({
|
|
139
|
+
totalClaims: z.number().int().nonnegative(),
|
|
140
|
+
supportedClaims: z.number().int().nonnegative(),
|
|
141
|
+
contradictedClaims: z.number().int().nonnegative(),
|
|
142
|
+
insufficientClaims: z.number().int().nonnegative(),
|
|
143
|
+
uncertainClaims: z.number().int().nonnegative(),
|
|
144
|
+
supportedRatio: z.number().min(0).max(1),
|
|
145
|
+
})
|
|
146
|
+
.strict();
|
|
147
|
+
|
|
148
|
+
export const claimVerificationResultSchema = z
|
|
149
|
+
.object({
|
|
150
|
+
schemaVersion: z.literal(CLAIM_VERIFICATION_SCHEMA_VERSION),
|
|
151
|
+
coordinateSpace: z.literal(CLAIM_COORDINATE_SPACE),
|
|
152
|
+
capsuleId: sha256Schema,
|
|
153
|
+
answerHash: sha256Schema,
|
|
154
|
+
coverageThreshold: z.literal(1),
|
|
155
|
+
claims: z.array(verifiedClaimSchema).max(256),
|
|
156
|
+
rejectedCitations: z.array(rejectedCitationSchema).max(256),
|
|
157
|
+
coverage: coverageSchema,
|
|
158
|
+
answerStatus: z.enum(["verified", "abstained"]),
|
|
159
|
+
abstained: z.boolean(),
|
|
160
|
+
abstentionReason: z
|
|
161
|
+
.enum([
|
|
162
|
+
"contradiction_detected",
|
|
163
|
+
"coverage_below_threshold",
|
|
164
|
+
"no_substantive_claims",
|
|
165
|
+
"citation_hygiene_failed",
|
|
166
|
+
])
|
|
167
|
+
.nullable(),
|
|
168
|
+
abstentionText: z.string().min(1).nullable(),
|
|
169
|
+
})
|
|
170
|
+
.strict()
|
|
171
|
+
.superRefine((value, context) => {
|
|
172
|
+
const counts = {
|
|
173
|
+
supportedClaims: value.claims.filter(
|
|
174
|
+
(claim) => claim.status === "supported"
|
|
175
|
+
).length,
|
|
176
|
+
contradictedClaims: value.claims.filter(
|
|
177
|
+
(claim) => claim.status === "contradicted"
|
|
178
|
+
).length,
|
|
179
|
+
insufficientClaims: value.claims.filter(
|
|
180
|
+
(claim) => claim.status === "insufficient"
|
|
181
|
+
).length,
|
|
182
|
+
uncertainClaims: value.claims.filter(
|
|
183
|
+
(claim) => claim.status === "uncertain"
|
|
184
|
+
).length,
|
|
185
|
+
};
|
|
186
|
+
const supportedRatio =
|
|
187
|
+
value.claims.length === 0
|
|
188
|
+
? 0
|
|
189
|
+
: counts.supportedClaims / value.claims.length;
|
|
190
|
+
const expectedReason =
|
|
191
|
+
value.claims.length === 0
|
|
192
|
+
? "no_substantive_claims"
|
|
193
|
+
: counts.contradictedClaims > 0
|
|
194
|
+
? "contradiction_detected"
|
|
195
|
+
: value.rejectedCitations.length > 0 ||
|
|
196
|
+
value.claims.some((claim) => claim.rejectedCitations.length > 0)
|
|
197
|
+
? "citation_hygiene_failed"
|
|
198
|
+
: supportedRatio < 1
|
|
199
|
+
? "coverage_below_threshold"
|
|
200
|
+
: null;
|
|
201
|
+
const coverageValid =
|
|
202
|
+
value.coverage.totalClaims === value.claims.length &&
|
|
203
|
+
Object.entries(counts).every(
|
|
204
|
+
([key, count]) => value.coverage[key as keyof typeof counts] === count
|
|
205
|
+
) &&
|
|
206
|
+
value.coverage.supportedRatio === supportedRatio;
|
|
207
|
+
const abstentionValid =
|
|
208
|
+
value.abstentionReason === expectedReason &&
|
|
209
|
+
value.abstained === (expectedReason !== null) &&
|
|
210
|
+
value.answerStatus ===
|
|
211
|
+
(expectedReason === null ? "verified" : "abstained") &&
|
|
212
|
+
(expectedReason === null
|
|
213
|
+
? value.abstentionText === null
|
|
214
|
+
: value.abstentionText !== null);
|
|
215
|
+
const spansValid = value.claims.every(
|
|
216
|
+
(claim, index) =>
|
|
217
|
+
index === 0 || claim.start >= (value.claims[index - 1]?.end ?? 0)
|
|
218
|
+
);
|
|
219
|
+
const claimIds = value.claims.map((claim) => claim.claimId);
|
|
220
|
+
if (
|
|
221
|
+
!coverageValid ||
|
|
222
|
+
!abstentionValid ||
|
|
223
|
+
!spansValid ||
|
|
224
|
+
new Set(claimIds).size !== claimIds.length
|
|
225
|
+
) {
|
|
226
|
+
context.addIssue({
|
|
227
|
+
code: "custom",
|
|
228
|
+
message: "claim verification aggregate is incoherent",
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
export type ClaimVerificationResult = z.infer<
|
|
234
|
+
typeof claimVerificationResultSchema
|
|
235
|
+
>;
|