@gmickel/gno 1.19.0 → 1.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -7
- package/assets/skill/SKILL.md +27 -12
- 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 +42 -17
- package/spec/evals-agentic.md +87 -5
- package/spec/mcp.md +53 -3
- package/spec/output-schemas/ask.schema.json +198 -0
- package/spec/output-schemas/claim-verification.schema.json +291 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +36 -1
- 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/program.ts +32 -1
- 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/retrieval-trace-evidence-origin.ts +3 -0
- package/src/core/retrieval-trace-session.ts +15 -2
- package/src/llm/errors.ts +10 -1
- package/src/llm/httpGeneration.ts +11 -1
- package/src/llm/nodeLlamaCpp/generation.ts +54 -10
- package/src/llm/types.ts +6 -0
- package/src/mcp/tools/ask.ts +228 -0
- package/src/mcp/tools/context.ts +28 -7
- package/src/mcp/tools/index.ts +9 -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 +35 -2
- 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/routes/api.ts +149 -3
|
@@ -18,6 +18,39 @@ type LlamaModel = Awaited<
|
|
|
18
18
|
Awaited<ReturnType<typeof import("node-llama-cpp").getLlama>>["loadModel"]
|
|
19
19
|
>
|
|
20
20
|
>;
|
|
21
|
+
type Llama = Awaited<ReturnType<typeof import("node-llama-cpp").getLlama>>;
|
|
22
|
+
type JsonGrammarSchema = Parameters<Llama["createGrammarForJsonSchema"]>[0];
|
|
23
|
+
|
|
24
|
+
export interface JsonSchemaGrammarLike {
|
|
25
|
+
parse(response: string): unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface StructuredPromptSession {
|
|
29
|
+
prompt(
|
|
30
|
+
prompt: string,
|
|
31
|
+
options: {
|
|
32
|
+
temperature: number;
|
|
33
|
+
seed: number;
|
|
34
|
+
maxTokens: number;
|
|
35
|
+
grammar?: JsonSchemaGrammarLike;
|
|
36
|
+
}
|
|
37
|
+
): Promise<string>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const promptWithJsonSchemaGrammar = async (
|
|
41
|
+
session: StructuredPromptSession,
|
|
42
|
+
prompt: string,
|
|
43
|
+
options: {
|
|
44
|
+
temperature: number;
|
|
45
|
+
seed: number;
|
|
46
|
+
maxTokens: number;
|
|
47
|
+
},
|
|
48
|
+
grammar?: JsonSchemaGrammarLike
|
|
49
|
+
): Promise<string> => {
|
|
50
|
+
const response = await session.prompt(prompt, { ...options, grammar });
|
|
51
|
+
grammar?.parse(response);
|
|
52
|
+
return response;
|
|
53
|
+
};
|
|
21
54
|
|
|
22
55
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
23
56
|
// Default Parameters (for determinism)
|
|
@@ -34,6 +67,7 @@ const DEFAULT_MAX_TOKENS = 256;
|
|
|
34
67
|
export class NodeLlamaCppGeneration implements GenerationPort {
|
|
35
68
|
private readonly manager: ModelManager;
|
|
36
69
|
readonly modelUri: string;
|
|
70
|
+
readonly structuredOutput = "json_schema" as const;
|
|
37
71
|
private readonly modelPath: string;
|
|
38
72
|
|
|
39
73
|
constructor(manager: ModelManager, modelUri: string, modelPath: string) {
|
|
@@ -56,11 +90,16 @@ export class NodeLlamaCppGeneration implements GenerationPort {
|
|
|
56
90
|
}
|
|
57
91
|
|
|
58
92
|
const llamaModel = model.value.model as LlamaModel;
|
|
59
|
-
|
|
60
|
-
params?.contextSize ? { contextSize: params.contextSize } : undefined
|
|
61
|
-
);
|
|
62
|
-
|
|
93
|
+
let context: Awaited<ReturnType<LlamaModel["createContext"]>> | null = null;
|
|
63
94
|
try {
|
|
95
|
+
const grammar = params?.jsonSchema
|
|
96
|
+
? await (
|
|
97
|
+
await this.manager.getLlama()
|
|
98
|
+
).createGrammarForJsonSchema(params.jsonSchema as JsonGrammarSchema)
|
|
99
|
+
: undefined;
|
|
100
|
+
context = await llamaModel.createContext(
|
|
101
|
+
params?.contextSize ? { contextSize: params.contextSize } : undefined
|
|
102
|
+
);
|
|
64
103
|
// Import LlamaChatSession dynamically
|
|
65
104
|
const { LlamaChatSession } = await import("node-llama-cpp");
|
|
66
105
|
const session = new LlamaChatSession({
|
|
@@ -68,17 +107,22 @@ export class NodeLlamaCppGeneration implements GenerationPort {
|
|
|
68
107
|
});
|
|
69
108
|
|
|
70
109
|
// Note: stop sequences not yet supported - requires stopOnTrigger API
|
|
71
|
-
const response = await
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
110
|
+
const response = await promptWithJsonSchemaGrammar(
|
|
111
|
+
session as StructuredPromptSession,
|
|
112
|
+
prompt,
|
|
113
|
+
{
|
|
114
|
+
temperature: params?.temperature ?? DEFAULT_TEMPERATURE,
|
|
115
|
+
seed: params?.seed ?? DEFAULT_SEED,
|
|
116
|
+
maxTokens: params?.maxTokens ?? DEFAULT_MAX_TOKENS,
|
|
117
|
+
},
|
|
118
|
+
grammar
|
|
119
|
+
);
|
|
76
120
|
|
|
77
121
|
return { ok: true, value: response };
|
|
78
122
|
} catch (e) {
|
|
79
123
|
return { ok: false, error: inferenceFailedError(this.modelUri, e) };
|
|
80
124
|
} finally {
|
|
81
|
-
await context
|
|
125
|
+
await context?.dispose().catch(() => {
|
|
82
126
|
// Ignore disposal errors
|
|
83
127
|
});
|
|
84
128
|
}
|
package/src/llm/types.ts
CHANGED
|
@@ -58,8 +58,12 @@ export interface GenParams {
|
|
|
58
58
|
contextSize?: number;
|
|
59
59
|
/** Stop sequences */
|
|
60
60
|
stop?: string[];
|
|
61
|
+
/** Closed JSON Schema enforced by a capable generation backend. */
|
|
62
|
+
jsonSchema?: Readonly<Record<string, unknown>>;
|
|
61
63
|
}
|
|
62
64
|
|
|
65
|
+
export type StructuredOutputCapability = "json_schema" | "none";
|
|
66
|
+
|
|
63
67
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
68
|
// Rerank Types
|
|
65
69
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -90,6 +94,8 @@ export interface EmbeddingPort {
|
|
|
90
94
|
|
|
91
95
|
export interface GenerationPort {
|
|
92
96
|
readonly modelUri: string;
|
|
97
|
+
/** Undefined is treated as unsupported for backwards-compatible ports. */
|
|
98
|
+
readonly structuredOutput?: StructuredOutputCapability;
|
|
93
99
|
generate(prompt: string, params?: GenParams): Promise<LlmResult<string>>;
|
|
94
100
|
dispose(): Promise<void>;
|
|
95
101
|
}
|
|
@@ -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
|
+
);
|
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,6 +19,7 @@ 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";
|
|
23
24
|
import { handleClearCollectionEmbeddings } from "./clear-collection-embeddings";
|
|
24
25
|
import { handleContext, handleContextVerify } from "./context";
|
|
@@ -96,6 +97,7 @@ export const MCP_TOOL_DESCRIPTIONS = {
|
|
|
96
97
|
"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
98
|
contextVerify:
|
|
98
99
|
"Verify a saved Context Capsule without rebuilding or mutating it. Reports unchanged, stale, missing, reranked, and fingerprint drift states against the active index.",
|
|
100
|
+
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
101
|
} as const;
|
|
100
102
|
|
|
101
103
|
/** Tool names whose execution mutates disk, config, or index state. */
|
|
@@ -978,6 +980,13 @@ export function registerTools(server: McpServer, ctx: ToolContext): void {
|
|
|
978
980
|
(args) => handleContextVerify(args, ctx)
|
|
979
981
|
);
|
|
980
982
|
|
|
983
|
+
server.tool(
|
|
984
|
+
"gno_ask",
|
|
985
|
+
MCP_TOOL_DESCRIPTIONS.ask,
|
|
986
|
+
askInputSchema.shape,
|
|
987
|
+
(args) => handleAsk(args, ctx)
|
|
988
|
+
);
|
|
989
|
+
|
|
981
990
|
server.tool(
|
|
982
991
|
"gno_search",
|
|
983
992
|
MCP_TOOL_DESCRIPTIONS.search,
|
|
@@ -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
|
+
>;
|