jev-agent-tools 0.2.0 → 0.3.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/CHANGELOG.md +37 -1
- package/CONTRIBUTING.md +3 -0
- package/README.md +22 -14
- package/SECURITY.md +17 -1
- package/dist/adapters/ask-files.js +11 -2
- package/dist/adapters/ask-proof.js +63 -7
- package/dist/adapters/command.js +82 -29
- package/dist/adapters/docs.js +30 -10
- package/dist/adapters/evidence-context.js +119 -0
- package/dist/adapters/files.js +141 -16
- package/dist/adapters/find.js +34 -6
- package/dist/adapters/git-base.js +7 -1
- package/dist/adapters/git.js +51 -7
- package/dist/adapters/locate-file.js +47 -9
- package/dist/adapters/private-storage.js +14 -6
- package/dist/adapters/risk-callers.js +3 -0
- package/dist/adapters/shell.js +23 -7
- package/dist/adapters/test-inventory.js +10 -2
- package/dist/configuration.js +17 -7
- package/dist/constants.js +26 -5
- package/dist/core/ask-references.js +193 -109
- package/dist/core/asks.js +78 -7
- package/dist/core/locate.js +8 -8
- package/dist/core/output.js +17 -0
- package/dist/core/result-report.js +302 -0
- package/dist/core/secret-path.js +34 -0
- package/dist/core/state.js +8 -1
- package/dist/core/units.js +1 -1
- package/dist/jev/client.js +34 -12
- package/dist/mcp/protocol.js +50 -27
- package/dist/mcp/tools.js +20 -7
- package/dist/render.js +72 -0
- package/dist/report-schema.js +1356 -0
- package/dist/result-types.js +1 -0
- package/dist/texts/ask-files.js +3 -1
- package/dist/texts/ask.js +3 -1
- package/dist/texts/check-diff.js +7 -4
- package/dist/texts/find.js +7 -2
- package/dist/texts/guide.js +3 -16
- package/dist/texts/instructions.js +72 -0
- package/dist/texts/locate.js +7 -2
- package/dist/texts/select-tests.js +3 -1
- package/dist/tools/ask-files.js +248 -15
- package/dist/tools/ask.js +523 -62
- package/dist/tools/check-diff.js +222 -30
- package/dist/tools/docs-check.js +122 -13
- package/dist/tools/find.js +320 -27
- package/dist/tools/locate.js +317 -18
- package/dist/tools/review-report.js +230 -0
- package/dist/tools/select-tests.js +273 -19
- package/dist/tools/spec-check.js +119 -22
- package/docs/adr/0001-strict-typescript-pure-core-offline-tests.md +3 -3
- package/docs/agent-instructions.md +59 -30
- package/docs/design.md +13 -1
- package/docs/mcp.md +8 -6
- package/docs/tools/jev_ask.md +8 -5
- package/docs/tools/jev_ask_files.md +2 -1
- package/docs/tools/jev_check_diff.md +4 -1
- package/docs/tools/jev_find_files.md +2 -1
- package/docs/tools/jev_locate_in_file.md +5 -0
- package/docs/tools/jev_select_tests.md +4 -1
- package/package.json +1 -1
- package/rules/jev-ask.md +22 -1
- package/server.json +2 -2
- package/src/adapters/ask-files.ts +11 -3
- package/src/adapters/ask-proof.ts +69 -11
- package/src/adapters/command.ts +96 -33
- package/src/adapters/docs.ts +33 -14
- package/src/adapters/evidence-context.ts +169 -0
- package/src/adapters/files.ts +146 -16
- package/src/adapters/find.ts +37 -7
- package/src/adapters/git-base.ts +7 -1
- package/src/adapters/git.ts +61 -8
- package/src/adapters/locate-file.ts +51 -9
- package/src/adapters/private-storage.ts +17 -5
- package/src/adapters/risk-callers.ts +3 -0
- package/src/adapters/shell.ts +23 -7
- package/src/adapters/test-inventory.ts +12 -4
- package/src/configuration.ts +16 -2
- package/src/constants.ts +26 -5
- package/src/core/ask-references.ts +262 -146
- package/src/core/asks.ts +79 -7
- package/src/core/import-boundaries.ts +8 -3
- package/src/core/locate.ts +8 -5
- package/src/core/output.ts +34 -0
- package/src/core/result-report.ts +410 -0
- package/src/core/secret-path.ts +37 -0
- package/src/core/state.ts +8 -1
- package/src/core/units.ts +3 -2
- package/src/index.ts +3 -0
- package/src/jev/client.ts +54 -16
- package/src/jev/types.ts +18 -3
- package/src/mcp/protocol.ts +91 -41
- package/src/mcp/tools.ts +26 -13
- package/src/render.ts +109 -0
- package/src/report-schema.ts +1380 -0
- package/src/result-types.ts +234 -0
- package/src/result.ts +4 -1
- package/src/runtime.ts +6 -0
- package/src/texts/ask-files.ts +4 -1
- package/src/texts/ask.ts +8 -1
- package/src/texts/check-diff.ts +7 -4
- package/src/texts/find.ts +8 -2
- package/src/texts/guide.ts +8 -16
- package/src/texts/instructions.ts +98 -0
- package/src/texts/locate.ts +8 -2
- package/src/texts/run-end.ts +2 -2
- package/src/texts/select-tests.ts +4 -1
- package/src/tools/ask-files.ts +309 -14
- package/src/tools/ask.ts +700 -77
- package/src/tools/check-diff.ts +331 -28
- package/src/tools/docs-check.ts +241 -39
- package/src/tools/find.ts +386 -29
- package/src/tools/locate.ts +384 -19
- package/src/tools/review-report.ts +308 -0
- package/src/tools/select-tests.ts +479 -21
- package/src/tools/spec-check.ts +193 -19
package/src/jev/client.ts
CHANGED
|
@@ -232,10 +232,19 @@ export function createJevClient(
|
|
|
232
232
|
options.onUsage?.(next.usage);
|
|
233
233
|
}
|
|
234
234
|
};
|
|
235
|
-
|
|
235
|
+
let stoppedCause: Extract<Answer, { type: "unjudged" }>["cause"];
|
|
236
|
+
const missing = (
|
|
237
|
+
ids: readonly string[],
|
|
238
|
+
reason: string,
|
|
239
|
+
cause: Extract<Answer, { type: "unjudged" }>["cause"] = stoppedCause,
|
|
240
|
+
) => {
|
|
236
241
|
const batchAnswers = Object.fromEntries(
|
|
237
|
-
ids.map((id) => [
|
|
242
|
+
ids.map((id) => [
|
|
243
|
+
id,
|
|
244
|
+
{ type: "unjudged" as const, reason, ...(cause ? { cause } : {}) },
|
|
245
|
+
]),
|
|
238
246
|
);
|
|
247
|
+
if (cause) meta.failureCause ??= cause;
|
|
239
248
|
for (const [id, answer] of Object.entries(batchAnswers))
|
|
240
249
|
if (!witnessSet.has(id) || !answers[id]) answers[id] = answer;
|
|
241
250
|
meta.batches?.push({ questionIds: [...ids], answers: batchAnswers });
|
|
@@ -281,6 +290,7 @@ export function createJevClient(
|
|
|
281
290
|
// must not replace the token refusal with a budget reason.
|
|
282
291
|
if (diagnostic) return;
|
|
283
292
|
stopped = admission.error;
|
|
293
|
+
stoppedCause = options.admissionCause?.();
|
|
284
294
|
missing(ids, stopped);
|
|
285
295
|
return;
|
|
286
296
|
}
|
|
@@ -322,12 +332,14 @@ export function createJevClient(
|
|
|
322
332
|
if (diagnostic) {
|
|
323
333
|
stopped =
|
|
324
334
|
"Jev max_tokens_exceeded: state too large for a single question; remaining questions not judged.";
|
|
335
|
+
stoppedCause = "provider_context_refusal";
|
|
325
336
|
return;
|
|
326
337
|
}
|
|
327
338
|
if (ids.length === 1) {
|
|
328
339
|
missing(
|
|
329
340
|
ids,
|
|
330
341
|
"Jev max_tokens_exceeded: state too large for a single question; question not judged.",
|
|
342
|
+
"provider_context_refusal",
|
|
331
343
|
);
|
|
332
344
|
return;
|
|
333
345
|
}
|
|
@@ -357,7 +369,7 @@ export function createJevClient(
|
|
|
357
369
|
const split = splitGroups(groups);
|
|
358
370
|
if (split.ok)
|
|
359
371
|
await Promise.all(split.halves.map((half) => send(half)));
|
|
360
|
-
else missing(ids, split.error);
|
|
372
|
+
else missing(ids, split.error, "provider_context_refusal");
|
|
361
373
|
return;
|
|
362
374
|
}
|
|
363
375
|
if (response.ok && isRecord(body)) {
|
|
@@ -366,7 +378,12 @@ export function createJevClient(
|
|
|
366
378
|
const batchAnswers = Object.fromEntries(
|
|
367
379
|
ids.map((id) => [
|
|
368
380
|
id,
|
|
369
|
-
|
|
381
|
+
(() => {
|
|
382
|
+
const answer = normalize(raw[id], selected[id] as Question);
|
|
383
|
+
return answer.type === "unjudged"
|
|
384
|
+
? { ...answer, cause: "invalid_response" as const }
|
|
385
|
+
: { ...answer, source: "fresh" as const };
|
|
386
|
+
})(),
|
|
370
387
|
]),
|
|
371
388
|
);
|
|
372
389
|
meta.batches?.push({ questionIds: ids, answers: batchAnswers });
|
|
@@ -416,7 +433,7 @@ export function createJevClient(
|
|
|
416
433
|
) {
|
|
417
434
|
const reason = `Jev HTTP 429, retry after ${retryAfter} s`;
|
|
418
435
|
failure ??= reason;
|
|
419
|
-
missing(ids, reason);
|
|
436
|
+
missing(ids, reason, "service_unavailable");
|
|
420
437
|
return;
|
|
421
438
|
}
|
|
422
439
|
const reason = response.ok
|
|
@@ -433,7 +450,11 @@ export function createJevClient(
|
|
|
433
450
|
failure ??= reason;
|
|
434
451
|
if (response.status === 401 || response.status === 403)
|
|
435
452
|
stopped = reason;
|
|
436
|
-
missing(
|
|
453
|
+
missing(
|
|
454
|
+
ids,
|
|
455
|
+
reason,
|
|
456
|
+
response.ok ? "invalid_response" : "service_unavailable",
|
|
457
|
+
);
|
|
437
458
|
return;
|
|
438
459
|
}
|
|
439
460
|
if (Number.isFinite(retryAfter) && retryAfter >= 0)
|
|
@@ -445,7 +466,11 @@ export function createJevClient(
|
|
|
445
466
|
const reason = `Jev request failed: ${String(error).replaceAll(config.apiKey, "[redacted]")}`;
|
|
446
467
|
if (options.signal?.aborted || attempt === REQUEST_ATTEMPTS - 1) {
|
|
447
468
|
failure ??= reason;
|
|
448
|
-
missing(
|
|
469
|
+
missing(
|
|
470
|
+
ids,
|
|
471
|
+
reason,
|
|
472
|
+
options.signal?.aborted ? "cancelled" : "transport_failure",
|
|
473
|
+
);
|
|
449
474
|
return;
|
|
450
475
|
}
|
|
451
476
|
} finally {
|
|
@@ -458,7 +483,11 @@ export function createJevClient(
|
|
|
458
483
|
} catch (error) {
|
|
459
484
|
const reason = `Jev request failed: ${String(error).replaceAll(config.apiKey, "[redacted]")}`;
|
|
460
485
|
failure ??= reason;
|
|
461
|
-
missing(
|
|
486
|
+
missing(
|
|
487
|
+
ids,
|
|
488
|
+
reason,
|
|
489
|
+
options.signal?.aborted ? "cancelled" : "transport_failure",
|
|
490
|
+
);
|
|
462
491
|
return;
|
|
463
492
|
}
|
|
464
493
|
}
|
|
@@ -473,13 +502,19 @@ export function createJevClient(
|
|
|
473
502
|
)
|
|
474
503
|
return true;
|
|
475
504
|
for (const id of group) {
|
|
476
|
-
answers[id] =
|
|
477
|
-
cache.get(keys.get(id) as string) as Answer,
|
|
478
|
-
|
|
505
|
+
answers[id] = {
|
|
506
|
+
...structuredClone(cache.get(keys.get(id) as string) as Answer),
|
|
507
|
+
source: "cache",
|
|
508
|
+
};
|
|
479
509
|
meta.cacheHits = (meta.cacheHits ?? 0) + 1;
|
|
480
510
|
}
|
|
481
|
-
const observedWitnesses =
|
|
482
|
-
|
|
511
|
+
const observedWitnesses = Object.fromEntries(
|
|
512
|
+
Object.entries(
|
|
513
|
+
structuredClone(witnessCache.get(witnessKey(group)) ?? {}),
|
|
514
|
+
).map(([id, answer]) => [
|
|
515
|
+
id,
|
|
516
|
+
{ ...answer, source: "cache" as const },
|
|
517
|
+
]),
|
|
483
518
|
);
|
|
484
519
|
for (const [id, answer] of Object.entries(observedWitnesses)) {
|
|
485
520
|
if (!answers[id]) {
|
|
@@ -495,9 +530,12 @@ export function createJevClient(
|
|
|
495
530
|
id,
|
|
496
531
|
witnessSet.has(id)
|
|
497
532
|
? (observedWitnesses[id] as Answer)
|
|
498
|
-
:
|
|
499
|
-
|
|
500
|
-
|
|
533
|
+
: {
|
|
534
|
+
...structuredClone(
|
|
535
|
+
cache.get(keys.get(id) as string) as Answer,
|
|
536
|
+
),
|
|
537
|
+
source: "cache" as const,
|
|
538
|
+
},
|
|
501
539
|
]),
|
|
502
540
|
),
|
|
503
541
|
});
|
package/src/jev/types.ts
CHANGED
|
@@ -15,8 +15,19 @@ export type Question =
|
|
|
15
15
|
}
|
|
16
16
|
| { type: "choice"; instructions: string; criteria: Record<string, string> }
|
|
17
17
|
| { type: "score"; instructions: string; criteria: string[] };
|
|
18
|
-
export type Answer =
|
|
19
|
-
| {
|
|
18
|
+
export type Answer = { source?: "fresh" | "cache" } & (
|
|
19
|
+
| {
|
|
20
|
+
type: "unjudged";
|
|
21
|
+
reason: string;
|
|
22
|
+
cause?:
|
|
23
|
+
| "call_budget"
|
|
24
|
+
| "session_budget"
|
|
25
|
+
| "provider_context_refusal"
|
|
26
|
+
| "invalid_response"
|
|
27
|
+
| "service_unavailable"
|
|
28
|
+
| "transport_failure"
|
|
29
|
+
| "cancelled";
|
|
30
|
+
}
|
|
20
31
|
| { type: "bool"; p: number }
|
|
21
32
|
| {
|
|
22
33
|
type: "choice";
|
|
@@ -30,8 +41,10 @@ export type Answer =
|
|
|
30
41
|
confidence: number;
|
|
31
42
|
legend: Json;
|
|
32
43
|
probabilities: Record<string, number>;
|
|
33
|
-
}
|
|
44
|
+
}
|
|
45
|
+
);
|
|
34
46
|
export interface JudgmentMetadata {
|
|
47
|
+
failureCause?: Extract<Answer, { type: "unjudged" }>["cause"];
|
|
35
48
|
usage?: { inputTokens: number; costUsd: number };
|
|
36
49
|
model?: string;
|
|
37
50
|
calls?: number;
|
|
@@ -48,6 +61,8 @@ export interface JudgmentOptions {
|
|
|
48
61
|
witnesses?: readonly string[];
|
|
49
62
|
cache?: boolean;
|
|
50
63
|
beforeRequest?: (questionCount: number) => Result<object>;
|
|
64
|
+
/** Structured cause of a denied beforeRequest admission, when established. */
|
|
65
|
+
admissionCause?: () => "call_budget" | "session_budget";
|
|
51
66
|
/**
|
|
52
67
|
* Awaited before each admission check. Resolves to a release function when
|
|
53
68
|
* it reserved a slot (a session under a USD limit), which the client calls
|
package/src/mcp/protocol.ts
CHANGED
|
@@ -24,6 +24,9 @@ export const INVALID_PARAMS = -32602;
|
|
|
24
24
|
export const INTERNAL_ERROR = -32603;
|
|
25
25
|
export const UNSUPPORTED_PROTOCOL_VERSION = -32022;
|
|
26
26
|
|
|
27
|
+
/** Input-schema failure, distinct from semantic tool refusals and server faults. */
|
|
28
|
+
export class McpInvalidParams extends Error {}
|
|
29
|
+
|
|
27
30
|
export interface McpContent {
|
|
28
31
|
type: "text";
|
|
29
32
|
text: string;
|
|
@@ -31,12 +34,14 @@ export interface McpContent {
|
|
|
31
34
|
export interface McpCallResult {
|
|
32
35
|
content: McpContent[];
|
|
33
36
|
isError?: boolean;
|
|
37
|
+
structuredContent?: Record<string, unknown>;
|
|
34
38
|
}
|
|
35
39
|
export interface McpTool {
|
|
36
40
|
name: string;
|
|
37
41
|
title?: string;
|
|
38
42
|
description: string;
|
|
39
43
|
inputSchema: { type: "object"; [key: string]: unknown };
|
|
44
|
+
outputSchema?: { type: "object"; [key: string]: unknown };
|
|
40
45
|
annotations?: {
|
|
41
46
|
title?: string;
|
|
42
47
|
readOnlyHint?: boolean;
|
|
@@ -78,6 +83,7 @@ export class McpServer {
|
|
|
78
83
|
private readonly tools: Map<string, McpTool>;
|
|
79
84
|
private readonly info: McpServerInfo;
|
|
80
85
|
private readonly inFlight = new Map<Id, AbortController>();
|
|
86
|
+
private negotiatedVersion: string = LATEST_INITIALIZE_VERSION;
|
|
81
87
|
|
|
82
88
|
constructor(info: McpServerInfo, tools: readonly McpTool[]) {
|
|
83
89
|
this.info = info;
|
|
@@ -134,41 +140,60 @@ export class McpServer {
|
|
|
134
140
|
requested,
|
|
135
141
|
},
|
|
136
142
|
);
|
|
143
|
+
const version =
|
|
144
|
+
typeof requested === "string"
|
|
145
|
+
? requested
|
|
146
|
+
: method === "server/discover"
|
|
147
|
+
? "2026-07-28"
|
|
148
|
+
: this.negotiatedVersion;
|
|
137
149
|
try {
|
|
138
150
|
switch (method) {
|
|
139
|
-
case "initialize":
|
|
140
|
-
|
|
151
|
+
case "initialize": {
|
|
152
|
+
const initialized = this.initialize(params);
|
|
153
|
+
return this.result(id, initialized, this.negotiatedVersion);
|
|
154
|
+
}
|
|
141
155
|
case "server/discover":
|
|
142
|
-
return this.result(
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
156
|
+
return this.result(
|
|
157
|
+
id,
|
|
158
|
+
{
|
|
159
|
+
supportedVersions: [...SUPPORTED_VERSIONS],
|
|
160
|
+
capabilities: { tools: { listChanged: false } },
|
|
161
|
+
_meta: {
|
|
162
|
+
"io.modelcontextprotocol/serverInfo": {
|
|
163
|
+
name: this.info.name,
|
|
164
|
+
version: this.info.version,
|
|
165
|
+
},
|
|
149
166
|
},
|
|
167
|
+
...(this.info.instructions
|
|
168
|
+
? { instructions: this.info.instructions }
|
|
169
|
+
: {}),
|
|
170
|
+
ttlMs: 0,
|
|
171
|
+
cacheScope: "private",
|
|
150
172
|
},
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
: {}),
|
|
154
|
-
ttlMs: 0,
|
|
155
|
-
cacheScope: "private",
|
|
156
|
-
});
|
|
173
|
+
version,
|
|
174
|
+
);
|
|
157
175
|
case "ping":
|
|
158
|
-
return this.result(id, {});
|
|
176
|
+
return this.result(id, {}, version);
|
|
159
177
|
case "tools/list":
|
|
160
|
-
return this.result(
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
178
|
+
return this.result(
|
|
179
|
+
id,
|
|
180
|
+
{
|
|
181
|
+
tools: [...this.tools.values()].map(
|
|
182
|
+
({ call: _call, outputSchema, ...tool }) => ({
|
|
183
|
+
...tool,
|
|
184
|
+
...(supportsStructuredResults(version) && outputSchema
|
|
185
|
+
? { outputSchema }
|
|
186
|
+
: {}),
|
|
187
|
+
}),
|
|
188
|
+
),
|
|
189
|
+
...(version === "2026-07-28"
|
|
190
|
+
? { ttlMs: 0, cacheScope: "private" }
|
|
191
|
+
: {}),
|
|
192
|
+
},
|
|
193
|
+
version,
|
|
194
|
+
);
|
|
170
195
|
case "tools/call":
|
|
171
|
-
return await this.callTool(id, params);
|
|
196
|
+
return await this.callTool(id, params, version);
|
|
172
197
|
default:
|
|
173
198
|
return this.error(
|
|
174
199
|
id,
|
|
@@ -194,6 +219,7 @@ export class McpServer {
|
|
|
194
219
|
(SUPPORTED_VERSIONS as readonly string[]).includes(requested)
|
|
195
220
|
? requested
|
|
196
221
|
: LATEST_INITIALIZE_VERSION;
|
|
222
|
+
this.negotiatedVersion = protocolVersion;
|
|
197
223
|
return {
|
|
198
224
|
protocolVersion,
|
|
199
225
|
capabilities: { tools: { listChanged: false } },
|
|
@@ -215,6 +241,7 @@ export class McpServer {
|
|
|
215
241
|
private async callTool(
|
|
216
242
|
id: Id,
|
|
217
243
|
params: Record<string, unknown>,
|
|
244
|
+
version: string,
|
|
218
245
|
): Promise<JsonRpcResponse | undefined> {
|
|
219
246
|
const tool =
|
|
220
247
|
typeof params.name === "string" ? this.tools.get(params.name) : undefined;
|
|
@@ -240,30 +267,45 @@ export class McpServer {
|
|
|
240
267
|
try {
|
|
241
268
|
const result = await tool.call(params.arguments ?? {}, controller.signal);
|
|
242
269
|
if (cancelled()) return undefined;
|
|
243
|
-
|
|
270
|
+
const { structuredContent, ...content } = result;
|
|
271
|
+
return this.result(
|
|
272
|
+
id,
|
|
273
|
+
{
|
|
274
|
+
...content,
|
|
275
|
+
...(supportsStructuredResults(version) && structuredContent
|
|
276
|
+
? { structuredContent }
|
|
277
|
+
: {}),
|
|
278
|
+
},
|
|
279
|
+
version,
|
|
280
|
+
);
|
|
244
281
|
} catch (error) {
|
|
245
282
|
if (cancelled()) return undefined;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
});
|
|
283
|
+
if (error instanceof McpInvalidParams)
|
|
284
|
+
return this.error(id, INVALID_PARAMS, error.message);
|
|
285
|
+
// Expected tool refusals already carry a report. An unexpected server
|
|
286
|
+
// fault has no trustworthy report/accounting; do not fabricate one.
|
|
287
|
+
return this.error(
|
|
288
|
+
id,
|
|
289
|
+
INTERNAL_ERROR,
|
|
290
|
+
`${tool.name} failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
291
|
+
);
|
|
256
292
|
} finally {
|
|
257
293
|
this.inFlight.delete(id);
|
|
258
294
|
}
|
|
259
295
|
}
|
|
260
296
|
|
|
261
|
-
private result(
|
|
262
|
-
|
|
297
|
+
private result(
|
|
298
|
+
id: Id,
|
|
299
|
+
result: Record<string, unknown>,
|
|
300
|
+
version: string,
|
|
301
|
+
): JsonRpcResponse {
|
|
263
302
|
return {
|
|
264
303
|
jsonrpc: "2.0",
|
|
265
304
|
id,
|
|
266
|
-
result: {
|
|
305
|
+
result: {
|
|
306
|
+
...(version === "2026-07-28" ? { resultType: "complete" } : {}),
|
|
307
|
+
...result,
|
|
308
|
+
},
|
|
267
309
|
};
|
|
268
310
|
}
|
|
269
311
|
|
|
@@ -280,3 +322,11 @@ export class McpServer {
|
|
|
280
322
|
};
|
|
281
323
|
}
|
|
282
324
|
}
|
|
325
|
+
|
|
326
|
+
function supportsStructuredResults(version: string): boolean {
|
|
327
|
+
return (
|
|
328
|
+
version === "2025-06-18" ||
|
|
329
|
+
version === "2025-11-25" ||
|
|
330
|
+
version === "2026-07-28"
|
|
331
|
+
);
|
|
332
|
+
}
|
package/src/mcp/tools.ts
CHANGED
|
@@ -4,18 +4,21 @@ import { spawnExec } from "../adapters/exec.ts";
|
|
|
4
4
|
import { ConfigController } from "../configuration.ts";
|
|
5
5
|
import { MCP_VALIDATION_MAX_ERRORS } from "../constants.ts";
|
|
6
6
|
import type { GitExec } from "../core/git.ts";
|
|
7
|
+
import { type ResultReportV1, resultIsError } from "../core/result-report.ts";
|
|
7
8
|
import { Guide } from "../guide.ts";
|
|
8
9
|
import { mcpHost } from "../host.ts";
|
|
9
10
|
import type { JevClient } from "../jev/types.ts";
|
|
11
|
+
import { mcpStructuredResultSchema } from "../report-schema.ts";
|
|
10
12
|
import type { ToolDependencies } from "../runtime.ts";
|
|
11
13
|
import { readSessionLimits, Session } from "../session.ts";
|
|
14
|
+
import { renderAgentInstructions } from "../texts/instructions.ts";
|
|
12
15
|
import { createAskTool } from "../tools/ask.ts";
|
|
13
16
|
import { createAskFilesTool } from "../tools/ask-files.ts";
|
|
14
17
|
import { createCheckDiffTool } from "../tools/check-diff.ts";
|
|
15
18
|
import { createFindFilesTool } from "../tools/find.ts";
|
|
16
19
|
import { createLocateTool } from "../tools/locate.ts";
|
|
17
20
|
import { createSelectTestsTool } from "../tools/select-tests.ts";
|
|
18
|
-
import type
|
|
21
|
+
import { McpInvalidParams, type McpTool } from "./protocol.ts";
|
|
19
22
|
|
|
20
23
|
/** The shape every tool factory already returns for pi/omp. */
|
|
21
24
|
interface HarnessTool {
|
|
@@ -30,7 +33,10 @@ interface HarnessTool {
|
|
|
30
33
|
signal: AbortSignal | undefined,
|
|
31
34
|
update: unknown,
|
|
32
35
|
ctx: { cwd: string },
|
|
33
|
-
): Promise<{
|
|
36
|
+
): Promise<{
|
|
37
|
+
content: { type: "text"; text: string }[];
|
|
38
|
+
details: { result: ResultReportV1 };
|
|
39
|
+
}>;
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
export interface McpToolOptions {
|
|
@@ -63,22 +69,26 @@ function validationError(schema: TSchema, value: unknown): string | undefined {
|
|
|
63
69
|
export async function loadMcpClient(
|
|
64
70
|
env: NodeJS.ProcessEnv,
|
|
65
71
|
configDirectory?: string,
|
|
66
|
-
): Promise<{ client?: JevClient; warning?: string }> {
|
|
72
|
+
): Promise<{ client?: JevClient; apiKey?: string; warning?: string }> {
|
|
67
73
|
try {
|
|
68
74
|
const controller = new ConfigController({
|
|
69
75
|
env,
|
|
70
76
|
...(configDirectory ? { directory: configDirectory } : {}),
|
|
71
77
|
});
|
|
78
|
+
const configured = () =>
|
|
79
|
+
controller.client
|
|
80
|
+
? { client: controller.client, apiKey: controller.values().apiKey }
|
|
81
|
+
: {};
|
|
72
82
|
try {
|
|
73
83
|
await controller.initialize({});
|
|
74
84
|
} catch (error) {
|
|
75
85
|
// Saved storage unusable: environment configuration (if any) still applies.
|
|
76
86
|
return {
|
|
77
|
-
...(
|
|
87
|
+
...configured(),
|
|
78
88
|
warning: error instanceof Error ? error.message : String(error),
|
|
79
89
|
};
|
|
80
90
|
}
|
|
81
|
-
return
|
|
91
|
+
return configured();
|
|
82
92
|
} catch (error) {
|
|
83
93
|
return { warning: error instanceof Error ? error.message : String(error) };
|
|
84
94
|
}
|
|
@@ -97,12 +107,14 @@ export async function createMcpTools(options: McpToolOptions): Promise<{
|
|
|
97
107
|
const env = options.env ?? process.env;
|
|
98
108
|
const host = mcpHost();
|
|
99
109
|
const loaded = options.client
|
|
100
|
-
? { client: options.client }
|
|
110
|
+
? { client: options.client, apiKey: env.JEV_TOOLS_API_KEY }
|
|
101
111
|
: await loadMcpClient(env, options.configDirectory);
|
|
102
112
|
const client = loaded.client;
|
|
103
113
|
const dependencies: ToolDependencies = {
|
|
104
114
|
client,
|
|
115
|
+
apiKey: loaded.apiKey,
|
|
105
116
|
host,
|
|
117
|
+
evidenceOrigin: "server",
|
|
106
118
|
runtime: {
|
|
107
119
|
session: new Session(readSessionLimits(env)),
|
|
108
120
|
guide: new Guide(host),
|
|
@@ -138,10 +150,10 @@ export async function createMcpTools(options: McpToolOptions): Promise<{
|
|
|
138
150
|
// Evidence is sent to the configured judgment endpoint.
|
|
139
151
|
openWorldHint: true,
|
|
140
152
|
},
|
|
153
|
+
outputSchema: mcpStructuredResultSchema as McpTool["inputSchema"],
|
|
141
154
|
async call(args, signal) {
|
|
142
155
|
const invalid = validationError(tool.parameters, args);
|
|
143
|
-
if (invalid)
|
|
144
|
-
return { content: [{ type: "text", text: invalid }], isError: true };
|
|
156
|
+
if (invalid) throw new McpInvalidParams(invalid);
|
|
145
157
|
const result = await tool.execute(
|
|
146
158
|
`mcp-${++id}`,
|
|
147
159
|
args as never,
|
|
@@ -149,14 +161,15 @@ export async function createMcpTools(options: McpToolOptions): Promise<{
|
|
|
149
161
|
undefined,
|
|
150
162
|
{ cwd: options.root },
|
|
151
163
|
);
|
|
152
|
-
return {
|
|
164
|
+
return {
|
|
165
|
+
content: result.content,
|
|
166
|
+
structuredContent: { result: result.details.result },
|
|
167
|
+
...(resultIsError(result.details.result) ? { isError: true } : {}),
|
|
168
|
+
};
|
|
153
169
|
},
|
|
154
170
|
};
|
|
155
171
|
});
|
|
156
|
-
const
|
|
157
|
-
const instructions = [dependencies.runtime.guide.text, ...guidelines].join(
|
|
158
|
-
"\n\n",
|
|
159
|
-
);
|
|
172
|
+
const instructions = renderAgentInstructions("mcp", host.names);
|
|
160
173
|
return {
|
|
161
174
|
tools,
|
|
162
175
|
instructions,
|
package/src/render.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Envelope, OutputLine } from "./core/output.ts";
|
|
2
|
+
import type { Knowledge, ResultReportV1, Scope } from "./core/result-report.ts";
|
|
2
3
|
|
|
3
4
|
function renderLine(line: OutputLine): string {
|
|
4
5
|
switch (line.type) {
|
|
@@ -43,3 +44,111 @@ function renderLine(line: OutputLine): string {
|
|
|
43
44
|
export function renderEnvelope(envelope: Envelope): string {
|
|
44
45
|
return envelope.lines.map(renderLine).join("\n");
|
|
45
46
|
}
|
|
47
|
+
|
|
48
|
+
function knowledge<T>(
|
|
49
|
+
value: Knowledge<T>,
|
|
50
|
+
format: (value: T) => string = String,
|
|
51
|
+
): string {
|
|
52
|
+
return value.status === "known"
|
|
53
|
+
? format(value.value)
|
|
54
|
+
: `${value.status} (${value.reason})`;
|
|
55
|
+
}
|
|
56
|
+
function scopeLabel(scope: Scope): string {
|
|
57
|
+
return scope.kind === "call"
|
|
58
|
+
? "call"
|
|
59
|
+
: scope.kind === "item"
|
|
60
|
+
? `items ${scope.itemIds.join(", ")}`
|
|
61
|
+
: scope.kind === "group"
|
|
62
|
+
? `groups ${scope.groupIds.join(", ")}`
|
|
63
|
+
: `inventory ${scope.inventoryIds.join(", ")}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Human projection uses the same typed facts as structured transport, never prose inference. */
|
|
67
|
+
export function renderResultReport(
|
|
68
|
+
report: ResultReportV1,
|
|
69
|
+
options: { details?: Envelope } = {},
|
|
70
|
+
): string {
|
|
71
|
+
const { context: c, accounting: a } = report;
|
|
72
|
+
const counts = a.requestedResults;
|
|
73
|
+
const mode =
|
|
74
|
+
counts.fresh + counts.cache
|
|
75
|
+
? `${counts.fresh} fresh / ${counts.cache} cache Jev results`
|
|
76
|
+
: counts.static
|
|
77
|
+
? "static selection; no Jev judgment"
|
|
78
|
+
: report.items.some((item) => item.selection?.selected)
|
|
79
|
+
? "conservative fallback selection; no Jev judgment"
|
|
80
|
+
: "no admissible judgment";
|
|
81
|
+
const lines = [
|
|
82
|
+
`${report.execution} — ${mode}${counts.notJudged ? `; ${counts.notJudged} not judged` : ""}`,
|
|
83
|
+
`context: authority ${knowledge(c.authority, (v) => `${v.path} (${v.origin})`)} · requested root ${knowledge(c.requestedRoot)} · effective root ${knowledge(c.effectiveRoot, (v) => `${v.path} (${v.origin}); common-dir ${knowledge(v.commonDir)}`)} · base ${knowledge(c.requestedBase)} → ${knowledge(c.resolvedBase)}`,
|
|
84
|
+
];
|
|
85
|
+
for (const inv of c.inventories) {
|
|
86
|
+
lines.push(
|
|
87
|
+
`inventory ${inv.id} (${inv.kind}): discovered ${knowledge(inv.discovered)} · considered ${knowledge(inv.considered)} · scopeRestricted ${inv.scopeRestricted} · rules ${inv.rules.join("; ")} · restrictions ${inv.restrictions.join("; ") || "none"}`,
|
|
88
|
+
);
|
|
89
|
+
for (const criterion of inv.criteria)
|
|
90
|
+
lines.push(
|
|
91
|
+
`criterion ${JSON.stringify(criterion.criterion)}: ${criterion.outcome}; matches ${knowledge(criterion.matches)}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (c.command.execution !== "not_requested")
|
|
95
|
+
lines.push(
|
|
96
|
+
`command: ${c.command.execution} · cwd ${knowledge(c.command.cwd)} · exit ${knowledge(c.command.exitCode)} · timedOut ${knowledge(c.command.timedOut)}`,
|
|
97
|
+
);
|
|
98
|
+
for (const item of report.items) {
|
|
99
|
+
if (item.treatment === "judged") {
|
|
100
|
+
const j = item.judgment;
|
|
101
|
+
lines.push(
|
|
102
|
+
`${j.band === "verdict" ? "" : `${j.band} `}${j.uncalibrated ? "uncalibrated " : ""}${item.label} = ${String(j.result)} (${j.measure.kind} ${knowledge(j.measure.value)}) [${item.source}]${j.reason ? ` — ${j.reason}` : ""}`,
|
|
103
|
+
);
|
|
104
|
+
for (const raw of j.rawValues)
|
|
105
|
+
lines.push(
|
|
106
|
+
` raw ${raw.label} = ${String(raw.value)}; probability ${knowledge(raw.probability)}`,
|
|
107
|
+
);
|
|
108
|
+
for (const control of j.controls) {
|
|
109
|
+
lines.push(
|
|
110
|
+
` control ${control.id} (${control.kind}, ${control.source}): ${control.outcome}`,
|
|
111
|
+
);
|
|
112
|
+
for (const raw of control.rawValues)
|
|
113
|
+
lines.push(
|
|
114
|
+
` raw ${raw.label} = ${String(raw.value)}; probability ${knowledge(raw.probability)}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
} else
|
|
118
|
+
lines.push(
|
|
119
|
+
`${item.treatment === "static" ? "static" : "unjudged"} ${item.label}${item.treatment === "static" ? ` — ${item.staticReason}` : ""}`,
|
|
120
|
+
);
|
|
121
|
+
for (const evidence of item.evidence)
|
|
122
|
+
lines.push(
|
|
123
|
+
` evidence ${evidence.target} (${evidence.side}): ${knowledge(evidence.canonicalPath)} · aliases ${evidence.aliases.join(", ") || "none"} · revision ${knowledge(evidence.revision)}`,
|
|
124
|
+
);
|
|
125
|
+
if (item.selection)
|
|
126
|
+
lines.push(
|
|
127
|
+
` selection: ${item.selection.selected ? "selected" : "not selected"} — ${item.selection.reason}`,
|
|
128
|
+
);
|
|
129
|
+
if (item.diagnosticIds.length)
|
|
130
|
+
lines.push(` diagnostics ${item.diagnosticIds.join(", ")}`);
|
|
131
|
+
if (item.actionIds.length)
|
|
132
|
+
lines.push(` actions ${item.actionIds.join(", ")}`);
|
|
133
|
+
}
|
|
134
|
+
// Complete diagnostic lists keep legacy text-only clients self-contained.
|
|
135
|
+
for (const d of report.diagnostics)
|
|
136
|
+
lines.push(
|
|
137
|
+
`${d.material ? "material " : ""}${d.effect} ${d.id} — ${d.cause}: ${d.fact} · target ${knowledge(d.target)} · origin ${d.origin} · scope ${scopeLabel(d.scope)} · members ${knowledge(d.memberCount)}${d.omittedMembers.length ? ` · omitted ${d.omittedMembers.join(", ")}` : ""} · actions ${d.actionIds.join(", ") || "none"}`,
|
|
138
|
+
);
|
|
139
|
+
for (const action of report.actions)
|
|
140
|
+
lines.push(
|
|
141
|
+
`next ${action.id} (${action.code}; ${scopeLabel(action.scope)}; target ${knowledge(action.target)}): ${action.condition} — ${action.instruction}`,
|
|
142
|
+
);
|
|
143
|
+
if (options.details) {
|
|
144
|
+
const detailLines = options.details.lines.filter(
|
|
145
|
+
(line) => line.type === "command" || line.type === "list",
|
|
146
|
+
);
|
|
147
|
+
if (detailLines.length)
|
|
148
|
+
lines.push("details:", renderEnvelope({ lines: detailLines }));
|
|
149
|
+
}
|
|
150
|
+
lines.push(
|
|
151
|
+
`HTTP attempts ${a.httpAttempts} · questions sent ${a.questionsSent} · requested results total ${knowledge(counts.total)} / fresh ${counts.fresh} / cache ${counts.cache} / not judged ${counts.notJudged} / static ${counts.static} · cache probes ${a.cacheHits}/${a.cacheRequests} · auxiliary controls fresh ${a.auxiliary.controls.fresh} / cache ${a.auxiliary.controls.cache} / not judged ${a.auxiliary.controls.notJudged} / static ${a.auxiliary.controls.static} · passages fresh ${a.auxiliary.passages.fresh} / cache ${a.auxiliary.passages.cache} / not judged ${a.auxiliary.passages.notJudged} · current cost USD ${knowledge(a.costUsd)} · elapsed ${a.elapsedMs} ms`,
|
|
152
|
+
);
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}
|