@vitest-agent/mcp 3.0.3 → 4.0.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 +7 -6
- package/annotations.js +18 -0
- package/bin/vitest-agent-mcp.js +5 -140
- package/{middleware/idempotency.js → idempotency.js} +48 -49
- package/index.d.ts +5610 -1549
- package/index.js +35 -17
- package/main.d.ts +31 -0
- package/main.js +144 -0
- package/package.json +9 -6
- package/prompts/layer.js +108 -0
- package/register-toolkit.js +311 -0
- package/server.js +24 -894
- package/{context.js → session.js} +38 -22
- package/toolkit.js +86 -0
- package/tools/acceptance-metrics.js +26 -14
- package/tools/cache-health.js +28 -17
- package/tools/commit-changes.js +33 -10
- package/tools/configure.js +33 -10
- package/tools/coverage.js +34 -5
- package/tools/errors.js +36 -26
- package/tools/failure-signature-get.js +33 -10
- package/tools/file-coverage.js +37 -13
- package/tools/help.js +45 -4
- package/tools/history.js +39 -19
- package/tools/hypothesis.js +118 -102
- package/tools/inventory.js +149 -146
- package/tools/note.js +131 -113
- package/tools/overview.js +33 -10
- package/tools/ping.js +22 -10
- package/tools/register-agent.js +88 -62
- package/tools/run-tests.js +76 -23
- package/tools/settings-list.js +27 -10
- package/tools/status.js +35 -10
- package/tools/tdd-artifact.js +37 -22
- package/tools/tdd-behavior.js +120 -108
- package/tools/tdd-goal.js +98 -85
- package/tools/tdd-phase-transition-request.js +201 -166
- package/tools/tdd-progress-push.js +102 -0
- package/tools/tdd-task.js +140 -138
- package/tools/test.js +152 -138
- package/tools/trends.js +37 -18
- package/tools/triage-brief.js +34 -15
- package/tools/turn-search.js +39 -16
- package/tools/wrapup-prompt.js +37 -17
- package/utils/crash-guards.js +0 -22
- package/utils/replay-marker.js +12 -0
- package/utils/safe-format-fatal-error.js +0 -16
- package/utils/tool-error-envelope.js +3 -3
- package/version.js +13 -0
- package/layers/McpLive.js +0 -29
- package/prompts/index.js +0 -89
- package/router.js +0 -74
- package/session-env.js +0 -112
- package/utils/effect-to-zod.js +0 -158
package/tools/test.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RenderText } from "../annotations.js";
|
|
2
2
|
import { collectProjectRows, resolveProjectTargets } from "./_project-groups.js";
|
|
3
3
|
import { Effect, Match, Option, Schema, SchemaGetter } from "effect";
|
|
4
|
-
import { DataReader } from "@vitest-agent/
|
|
4
|
+
import { DataReader } from "@vitest-agent/engine";
|
|
5
|
+
import { Tool } from "effect/unstable/ai";
|
|
5
6
|
|
|
6
7
|
//#region src/tools/test.ts
|
|
7
8
|
const TestRowSchema = Schema.Struct({
|
|
@@ -141,6 +142,11 @@ const TestArtifactsResult = Schema.Struct({
|
|
|
141
142
|
count: Schema.Number,
|
|
142
143
|
artifacts: Schema.Array(ArtifactRowSchema)
|
|
143
144
|
}).annotate({ identifier: "TestArtifacts" });
|
|
145
|
+
/**
|
|
146
|
+
* The `test` tool's success payload.
|
|
147
|
+
*
|
|
148
|
+
* @public
|
|
149
|
+
*/
|
|
144
150
|
const TestResult = Schema.Union([
|
|
145
151
|
TestListResult,
|
|
146
152
|
TestGetFound,
|
|
@@ -297,40 +303,47 @@ const TestAsMarkdown = TestResult.pipe(Schema.decodeTo(Schema.String, {
|
|
|
297
303
|
}));
|
|
298
304
|
const ListVariant = Schema.Struct({
|
|
299
305
|
action: Schema.Literal("list"),
|
|
300
|
-
project: Schema.
|
|
301
|
-
state: Schema.
|
|
302
|
-
module: Schema.
|
|
303
|
-
limit: Schema.
|
|
306
|
+
project: Schema.optionalKey(Schema.String),
|
|
307
|
+
state: Schema.optionalKey(Schema.String).annotate({ description: "list: filter by state" }),
|
|
308
|
+
module: Schema.optionalKey(Schema.String).annotate({ description: "list: filter by module path" }),
|
|
309
|
+
limit: Schema.optionalKey(Schema.Finite).annotate({ description: "list: max rows to return" })
|
|
304
310
|
});
|
|
305
311
|
const GetVariant = Schema.Struct({
|
|
306
312
|
action: Schema.Literal("get"),
|
|
307
|
-
fullName: Schema.String,
|
|
308
|
-
project: Schema.
|
|
309
|
-
modulePath: Schema.
|
|
313
|
+
fullName: Schema.String.annotate({ description: "get / annotations / artifacts: full test name" }),
|
|
314
|
+
project: Schema.optionalKey(Schema.String),
|
|
315
|
+
modulePath: Schema.optionalKey(Schema.String).annotate({ description: "Exact module_path match — disambiguates a fullName that exists in more than one test file." })
|
|
310
316
|
});
|
|
311
317
|
const ForFileVariant = Schema.Struct({
|
|
312
318
|
action: Schema.Literal("for_file"),
|
|
313
|
-
filePath: Schema.String
|
|
319
|
+
filePath: Schema.String.annotate({ description: "for_file: source file path" })
|
|
314
320
|
});
|
|
315
321
|
const ForTagVariant = Schema.Struct({
|
|
316
322
|
action: Schema.Literal("for_tag"),
|
|
317
|
-
tag: Schema.String,
|
|
318
|
-
project: Schema.
|
|
323
|
+
tag: Schema.String.annotate({ description: "for_tag: tag name" }),
|
|
324
|
+
project: Schema.optionalKey(Schema.String)
|
|
319
325
|
});
|
|
320
326
|
const AnnotationsVariant = Schema.Struct({
|
|
321
327
|
action: Schema.Literal("annotations"),
|
|
322
|
-
fullName: Schema.String,
|
|
323
|
-
project: Schema.
|
|
324
|
-
modulePath: Schema.
|
|
325
|
-
maxBytes: Schema.
|
|
328
|
+
fullName: Schema.String.annotate({ description: "get / annotations / artifacts: full test name" }),
|
|
329
|
+
project: Schema.optionalKey(Schema.String),
|
|
330
|
+
modulePath: Schema.optionalKey(Schema.String).annotate({ description: "Exact module_path match — disambiguates a fullName that exists in more than one test file." }),
|
|
331
|
+
maxBytes: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).annotate({ description: "Total byte budget for inline attachment bodies across the whole response. Must be a non-negative integer. Defaults to 0 — descriptors only, no bodies." }))
|
|
326
332
|
});
|
|
327
333
|
const ArtifactsVariant = Schema.Struct({
|
|
328
334
|
action: Schema.Literal("artifacts"),
|
|
329
|
-
fullName: Schema.String,
|
|
330
|
-
project: Schema.
|
|
331
|
-
modulePath: Schema.
|
|
332
|
-
maxBytes: Schema.
|
|
335
|
+
fullName: Schema.String.annotate({ description: "get / annotations / artifacts: full test name" }),
|
|
336
|
+
project: Schema.optionalKey(Schema.String),
|
|
337
|
+
modulePath: Schema.optionalKey(Schema.String).annotate({ description: "Exact module_path match — disambiguates a fullName that exists in more than one test file." }),
|
|
338
|
+
maxBytes: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).annotate({ description: "Total byte budget for inline attachment bodies across the whole response. Must be a non-negative integer. Defaults to 0 — descriptors only, no bodies." }))
|
|
333
339
|
});
|
|
340
|
+
/**
|
|
341
|
+
* The `test` tool's parameters — a union discriminated on `action`. The
|
|
342
|
+
* served JSON Schema is a `oneOf` over the variants with
|
|
343
|
+
* `x-discriminator: "action"`.
|
|
344
|
+
*
|
|
345
|
+
* @public
|
|
346
|
+
*/
|
|
334
347
|
const TestInput = Schema.Union([
|
|
335
348
|
ListVariant,
|
|
336
349
|
GetVariant,
|
|
@@ -340,132 +353,133 @@ const TestInput = Schema.Union([
|
|
|
340
353
|
ArtifactsVariant
|
|
341
354
|
]);
|
|
342
355
|
/**
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
356
|
+
* Handler for {@link testTool}.
|
|
357
|
+
*
|
|
358
|
+
* @public
|
|
346
359
|
*/
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
};
|
|
374
|
-
}),
|
|
375
|
-
get: (variant) => Effect.gen(function* () {
|
|
376
|
-
const reader = yield* DataReader;
|
|
377
|
-
const candidates = variant.project ? [variant.project] : yield* reader.getRunsByProject().pipe(Effect.map((rs) => rs.map((r) => r.project)));
|
|
378
|
-
for (const project of candidates) {
|
|
379
|
-
const modules = yield* reader.getTestModulesByFullName(project, variant.fullName);
|
|
380
|
-
if (modules.length === 0) continue;
|
|
381
|
-
if (variant.modulePath === void 0 && modules.length > 1) return {
|
|
382
|
-
action: "get",
|
|
383
|
-
found: false,
|
|
384
|
-
project,
|
|
385
|
-
fullName: variant.fullName,
|
|
386
|
-
ambiguous: true,
|
|
387
|
-
candidateModules: modules
|
|
388
|
-
};
|
|
389
|
-
const testOpt = yield* reader.getTestByFullName(project, variant.fullName, { ...variant.modulePath !== void 0 && { modulePath: variant.modulePath } });
|
|
390
|
-
if (Option.isNone(testOpt)) continue;
|
|
391
|
-
const matchingErrors = (yield* reader.getErrors(project)).filter((e) => e.testFullName === variant.fullName && e.moduleFile === testOpt.value.module).map((e) => ({
|
|
392
|
-
name: e.name,
|
|
393
|
-
message: e.message,
|
|
394
|
-
diff: e.diff,
|
|
395
|
-
stack: e.stack
|
|
396
|
-
}));
|
|
397
|
-
const testHistory = (yield* reader.getHistory(project, {
|
|
398
|
-
testName: variant.fullName,
|
|
399
|
-
modulePath: testOpt.value.module
|
|
400
|
-
})).tests.find((entry) => entry.fullName === variant.fullName);
|
|
401
|
-
return {
|
|
402
|
-
action: "get",
|
|
403
|
-
found: true,
|
|
404
|
-
project,
|
|
405
|
-
test: testOpt.value,
|
|
406
|
-
errors: matchingErrors,
|
|
407
|
-
runs: testHistory ? testHistory.runs : []
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
return {
|
|
360
|
+
const handleTest = (input) => Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
|
|
361
|
+
list: (variant) => Effect.gen(function* () {
|
|
362
|
+
const reader = yield* DataReader;
|
|
363
|
+
const opts = {};
|
|
364
|
+
if (variant.state !== void 0) opts.state = variant.state;
|
|
365
|
+
if (variant.module !== void 0) opts.module = variant.module;
|
|
366
|
+
if (variant.limit !== void 0) opts.limit = variant.limit;
|
|
367
|
+
const targets = yield* resolveProjectTargets(variant.project, () => reader.getRunsByProject());
|
|
368
|
+
const grouped = yield* collectProjectRows(targets, (project) => reader.listTests(project, opts));
|
|
369
|
+
const groups = grouped.groups.map((group) => ({
|
|
370
|
+
project: group.project,
|
|
371
|
+
tests: group.rows
|
|
372
|
+
}));
|
|
373
|
+
return {
|
|
374
|
+
action: "list",
|
|
375
|
+
count: grouped.total,
|
|
376
|
+
groups
|
|
377
|
+
};
|
|
378
|
+
}),
|
|
379
|
+
get: (variant) => Effect.gen(function* () {
|
|
380
|
+
const reader = yield* DataReader;
|
|
381
|
+
const candidates = variant.project ? [variant.project] : yield* reader.getRunsByProject().pipe(Effect.map((rs) => rs.map((r) => r.project)));
|
|
382
|
+
for (const project of candidates) {
|
|
383
|
+
const modules = yield* reader.getTestModulesByFullName(project, variant.fullName);
|
|
384
|
+
if (modules.length === 0) continue;
|
|
385
|
+
if (variant.modulePath === void 0 && modules.length > 1) return {
|
|
411
386
|
action: "get",
|
|
412
387
|
found: false,
|
|
413
|
-
project: variant.project ?? candidates[0] ?? "",
|
|
414
|
-
fullName: variant.fullName
|
|
415
|
-
};
|
|
416
|
-
}),
|
|
417
|
-
for_file: (variant) => Effect.gen(function* () {
|
|
418
|
-
const testFiles = yield* (yield* DataReader).getTestsForFile(variant.filePath);
|
|
419
|
-
return {
|
|
420
|
-
action: "for_file",
|
|
421
|
-
filePath: variant.filePath,
|
|
422
|
-
count: testFiles.length,
|
|
423
|
-
testFiles
|
|
424
|
-
};
|
|
425
|
-
}),
|
|
426
|
-
for_tag: (variant) => Effect.gen(function* () {
|
|
427
|
-
const reader = yield* DataReader;
|
|
428
|
-
const targets = yield* resolveProjectTargets(variant.project, () => reader.getRunsByProject());
|
|
429
|
-
const grouped = yield* collectProjectRows(targets, (project) => reader.listTestsForTag(variant.tag, { project }));
|
|
430
|
-
const groups = grouped.groups.map((group) => ({
|
|
431
|
-
project: group.project,
|
|
432
|
-
tests: group.rows
|
|
433
|
-
}));
|
|
434
|
-
return {
|
|
435
|
-
action: "for_tag",
|
|
436
|
-
tag: variant.tag,
|
|
437
|
-
count: grouped.total,
|
|
438
|
-
groups
|
|
439
|
-
};
|
|
440
|
-
}),
|
|
441
|
-
annotations: (variant) => Effect.gen(function* () {
|
|
442
|
-
const reader = yield* DataReader;
|
|
443
|
-
const project = variant.project ?? (yield* reader.getRunsByProject().pipe(Effect.map((rs) => rs[0]?.project ?? "")));
|
|
444
|
-
const rows = yield* reader.getAnnotationsForTest(project, variant.fullName, { ...variant.modulePath !== void 0 && { modulePath: variant.modulePath } });
|
|
445
|
-
const annotations = applyBodyBudget(rows, variant.maxBytes ?? 0);
|
|
446
|
-
return {
|
|
447
|
-
action: "annotations",
|
|
448
388
|
project,
|
|
449
389
|
fullName: variant.fullName,
|
|
450
|
-
|
|
451
|
-
|
|
390
|
+
ambiguous: true,
|
|
391
|
+
candidateModules: modules
|
|
452
392
|
};
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
393
|
+
const testOpt = yield* reader.getTestByFullName(project, variant.fullName, { ...variant.modulePath !== void 0 && { modulePath: variant.modulePath } });
|
|
394
|
+
if (Option.isNone(testOpt)) continue;
|
|
395
|
+
const matchingErrors = (yield* reader.getErrors(project)).filter((e) => e.testFullName === variant.fullName && e.moduleFile === testOpt.value.module).map((e) => ({
|
|
396
|
+
name: e.name,
|
|
397
|
+
message: e.message,
|
|
398
|
+
diff: e.diff,
|
|
399
|
+
stack: e.stack
|
|
400
|
+
}));
|
|
401
|
+
const testHistory = (yield* reader.getHistory(project, {
|
|
402
|
+
testName: variant.fullName,
|
|
403
|
+
modulePath: testOpt.value.module
|
|
404
|
+
})).tests.find((entry) => entry.fullName === variant.fullName);
|
|
459
405
|
return {
|
|
460
|
-
action: "
|
|
406
|
+
action: "get",
|
|
407
|
+
found: true,
|
|
461
408
|
project,
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
409
|
+
test: testOpt.value,
|
|
410
|
+
errors: matchingErrors,
|
|
411
|
+
runs: testHistory ? testHistory.runs : []
|
|
465
412
|
};
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
action: "get",
|
|
416
|
+
found: false,
|
|
417
|
+
project: variant.project ?? candidates[0] ?? "",
|
|
418
|
+
fullName: variant.fullName
|
|
419
|
+
};
|
|
420
|
+
}),
|
|
421
|
+
for_file: (variant) => Effect.gen(function* () {
|
|
422
|
+
const testFiles = yield* (yield* DataReader).getTestsForFile(variant.filePath);
|
|
423
|
+
return {
|
|
424
|
+
action: "for_file",
|
|
425
|
+
filePath: variant.filePath,
|
|
426
|
+
count: testFiles.length,
|
|
427
|
+
testFiles
|
|
428
|
+
};
|
|
429
|
+
}),
|
|
430
|
+
for_tag: (variant) => Effect.gen(function* () {
|
|
431
|
+
const reader = yield* DataReader;
|
|
432
|
+
const targets = yield* resolveProjectTargets(variant.project, () => reader.getRunsByProject());
|
|
433
|
+
const grouped = yield* collectProjectRows(targets, (project) => reader.listTestsForTag(variant.tag, { project }));
|
|
434
|
+
const groups = grouped.groups.map((group) => ({
|
|
435
|
+
project: group.project,
|
|
436
|
+
tests: group.rows
|
|
437
|
+
}));
|
|
438
|
+
return {
|
|
439
|
+
action: "for_tag",
|
|
440
|
+
tag: variant.tag,
|
|
441
|
+
count: grouped.total,
|
|
442
|
+
groups
|
|
443
|
+
};
|
|
444
|
+
}),
|
|
445
|
+
annotations: (variant) => Effect.gen(function* () {
|
|
446
|
+
const reader = yield* DataReader;
|
|
447
|
+
const project = variant.project ?? (yield* reader.getRunsByProject().pipe(Effect.map((rs) => rs[0]?.project ?? "")));
|
|
448
|
+
const rows = yield* reader.getAnnotationsForTest(project, variant.fullName, { ...variant.modulePath !== void 0 && { modulePath: variant.modulePath } });
|
|
449
|
+
const annotations = applyBodyBudget(rows, variant.maxBytes ?? 0);
|
|
450
|
+
return {
|
|
451
|
+
action: "annotations",
|
|
452
|
+
project,
|
|
453
|
+
fullName: variant.fullName,
|
|
454
|
+
count: annotations.length,
|
|
455
|
+
annotations
|
|
456
|
+
};
|
|
457
|
+
}),
|
|
458
|
+
artifacts: (variant) => Effect.gen(function* () {
|
|
459
|
+
const reader = yield* DataReader;
|
|
460
|
+
const project = variant.project ?? (yield* reader.getRunsByProject().pipe(Effect.map((rs) => rs[0]?.project ?? "")));
|
|
461
|
+
const rows = yield* reader.getArtifactsForTest(project, variant.fullName, { ...variant.modulePath !== void 0 && { modulePath: variant.modulePath } });
|
|
462
|
+
const artifacts = applyBodyBudget(rows, variant.maxBytes ?? 0);
|
|
463
|
+
return {
|
|
464
|
+
action: "artifacts",
|
|
465
|
+
project,
|
|
466
|
+
fullName: variant.fullName,
|
|
467
|
+
count: artifacts.length,
|
|
468
|
+
artifacts
|
|
469
|
+
};
|
|
470
|
+
})
|
|
471
|
+
})).pipe(Effect.orDie);
|
|
472
|
+
/**
|
|
473
|
+
* The Effect-native `test` tool.
|
|
474
|
+
*
|
|
475
|
+
* @public
|
|
476
|
+
*/
|
|
477
|
+
const testTool = Tool.make("test", {
|
|
478
|
+
description: "Use to inspect tests, with an action discriminator: action='list' (project?, state?, module?, limit?) returns matching tests; action='get' (fullName, project?, modulePath?) returns details + errors + run history — a fullName that exists in more than one module returns found=false with ambiguous=true and candidateModules[], so pass modulePath to disambiguate; action='for_file' (filePath) returns test modules covering a source file; action='for_tag' (tag, project?) returns tests carrying a tag, grouped by project; action='annotations' (fullName, project?, modulePath?) returns the test annotations the author recorded via context.annotate; action='artifacts' (fullName, project?, modulePath?) returns the test artifacts recorded for the test — both return attachment descriptors (contentType, path, byteSize) and omit inline bodies unless maxBytes (a non-negative integer byte budget for the whole response, default 0) is passed, and neither has anything to do with TDD artifacts (see tdd_artifact_list). structuredContent carries the typed payload (discriminate on `action`, then on `found` for get).",
|
|
479
|
+
parameters: TestInput,
|
|
480
|
+
success: TestResult,
|
|
481
|
+
dependencies: [DataReader]
|
|
482
|
+
}).annotate(Tool.Title, "Test").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatTestMarkdown(encoded));
|
|
469
483
|
|
|
470
484
|
//#endregion
|
|
471
|
-
export {
|
|
485
|
+
export { TestInput, TestResult, formatTestMarkdown, handleTest, testTool };
|
package/tools/trends.js
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RenderText } from "../annotations.js";
|
|
2
2
|
import { Effect, Option, Schema, SchemaGetter } from "effect";
|
|
3
|
-
import { DataReader
|
|
3
|
+
import { DataReader } from "@vitest-agent/engine";
|
|
4
|
+
import { Tool } from "effect/unstable/ai";
|
|
5
|
+
import { TrendRecord } from "@vitest-agent/sdk";
|
|
4
6
|
|
|
5
7
|
//#region src/tools/trends.ts
|
|
6
|
-
/**
|
|
7
|
-
* `test_trends` MCP tool — Schema-driven implementation.
|
|
8
|
-
*
|
|
9
|
-
* Wraps the existing `TrendRecord` Schema in a result envelope that
|
|
10
|
-
* carries the project name and a `dataAvailable` flag, so callers
|
|
11
|
-
* can distinguish "no trend data yet" from "data plus rendering"
|
|
12
|
-
* without parsing prose.
|
|
13
|
-
*
|
|
14
|
-
* @packageDocumentation
|
|
15
|
-
*/
|
|
16
8
|
const TrendsAvailable = Schema.Struct({
|
|
17
9
|
dataAvailable: Schema.Literal(true).annotate({ description: "Discriminant — `true` when at least one trend entry exists for the project." }),
|
|
18
10
|
project: Schema.String,
|
|
@@ -22,6 +14,11 @@ const TrendsAbsent = Schema.Struct({
|
|
|
22
14
|
dataAvailable: Schema.Literal(false).annotate({ description: "Discriminant — `false` when fewer than two runs have been recorded for the project." }),
|
|
23
15
|
project: Schema.String
|
|
24
16
|
}).annotate({ identifier: "TestTrendsAbsent" });
|
|
17
|
+
/**
|
|
18
|
+
* The `test_trends` tool's success payload.
|
|
19
|
+
*
|
|
20
|
+
* @public
|
|
21
|
+
*/
|
|
25
22
|
const TestTrendsResult = Schema.Union([TrendsAvailable, TrendsAbsent]).annotate({
|
|
26
23
|
identifier: "TestTrendsResult",
|
|
27
24
|
title: "test_trends result",
|
|
@@ -92,10 +89,21 @@ const TestTrendsAsMarkdown = TestTrendsResult.pipe(Schema.decodeTo(Schema.String
|
|
|
92
89
|
decode: SchemaGetter.transform((data) => formatTestTrendsMarkdown(data)),
|
|
93
90
|
encode: SchemaGetter.forbidden(() => "TestTrendsAsMarkdown is one-way: markdown cannot be parsed back to TestTrendsResult.")
|
|
94
91
|
}));
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
92
|
+
/**
|
|
93
|
+
* The `test_trends` tool's parameters.
|
|
94
|
+
*
|
|
95
|
+
* @public
|
|
96
|
+
*/
|
|
97
|
+
const TestTrendsInput = Schema.Struct({
|
|
98
|
+
project: Schema.String.annotate({ description: "Project name (required)" }),
|
|
99
|
+
limit: Schema.optionalKey(Schema.Finite).annotate({ description: "Max number of trend entries to return" })
|
|
100
|
+
});
|
|
101
|
+
/**
|
|
102
|
+
* Handler for {@link testTrendsTool}.
|
|
103
|
+
*
|
|
104
|
+
* @public
|
|
105
|
+
*/
|
|
106
|
+
const handleTestTrends = (input) => Effect.gen(function* () {
|
|
99
107
|
const trendsOpt = yield* (yield* DataReader).getTrends(input.project, input.limit);
|
|
100
108
|
if (Option.isNone(trendsOpt) || trendsOpt.value.entries.length === 0) return {
|
|
101
109
|
dataAvailable: false,
|
|
@@ -106,7 +114,18 @@ const testTrends = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct
|
|
|
106
114
|
project: input.project,
|
|
107
115
|
trends: trendsOpt.value
|
|
108
116
|
};
|
|
109
|
-
}))
|
|
117
|
+
}).pipe(Effect.orDie);
|
|
118
|
+
/**
|
|
119
|
+
* The Effect-native `test_trends` tool.
|
|
120
|
+
*
|
|
121
|
+
* @public
|
|
122
|
+
*/
|
|
123
|
+
const testTrendsTool = Tool.make("test_trends", {
|
|
124
|
+
description: "Use when you want to see whether a project's coverage is trending up or down over time. Returns markdown in content[] and a typed JSON object in structuredContent ({ dataAvailable, project, trends? }).",
|
|
125
|
+
parameters: TestTrendsInput,
|
|
126
|
+
success: TestTrendsResult,
|
|
127
|
+
dependencies: [DataReader]
|
|
128
|
+
}).annotate(Tool.Title, "Test trends").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatTestTrendsMarkdown(encoded));
|
|
110
129
|
|
|
111
130
|
//#endregion
|
|
112
|
-
export {
|
|
131
|
+
export { TestTrendsInput, TestTrendsResult, formatTestTrendsMarkdown, handleTestTrends, testTrendsTool };
|
package/tools/triage-brief.js
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RenderText } from "../annotations.js";
|
|
2
2
|
import { Effect, Schema } from "effect";
|
|
3
|
-
import { formatTriageEffect } from "@vitest-agent/
|
|
3
|
+
import { DataReader, formatTriageEffect } from "@vitest-agent/engine";
|
|
4
|
+
import { Tool } from "effect/unstable/ai";
|
|
4
5
|
|
|
5
6
|
//#region src/tools/triage-brief.ts
|
|
6
7
|
/**
|
|
7
|
-
* `triage_brief`
|
|
8
|
+
* The `triage_brief` tool's success payload.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
* rendering since this is a narrative tool — there's no underlying
|
|
11
|
-
* record set the agent would parse separately. The `hasContent` flag
|
|
12
|
-
* lets callers branch on the cold-start case without grepping prose.
|
|
13
|
-
*
|
|
14
|
-
* @packageDocumentation
|
|
10
|
+
* @public
|
|
15
11
|
*/
|
|
16
12
|
const TriageBriefResult = Schema.Struct({
|
|
17
13
|
hasContent: Schema.Boolean.annotate({ description: "`false` when no orientation signal is available yet (run tests to populate)." }),
|
|
@@ -21,10 +17,21 @@ const TriageBriefResult = Schema.Struct({
|
|
|
21
17
|
title: "triage_brief result",
|
|
22
18
|
description: "Orientation triage envelope. Branch on `hasContent` for cold-start; consume `markdown` for rendering."
|
|
23
19
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
/**
|
|
21
|
+
* The `triage_brief` tool's parameters.
|
|
22
|
+
*
|
|
23
|
+
* @public
|
|
24
|
+
*/
|
|
25
|
+
const TriageBriefInput = Schema.Struct({
|
|
26
|
+
project: Schema.optionalKey(Schema.String).annotate({ description: "Filter to a specific project" }),
|
|
27
|
+
maxLines: Schema.optionalKey(Schema.Finite).annotate({ description: "Soft cap on rendered output lines" })
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* Handler for {@link triageBriefTool}.
|
|
31
|
+
*
|
|
32
|
+
* @public
|
|
33
|
+
*/
|
|
34
|
+
const handleTriageBrief = (input) => Effect.gen(function* () {
|
|
28
35
|
const md = yield* formatTriageEffect({
|
|
29
36
|
...input.project !== void 0 && { project: input.project },
|
|
30
37
|
...input.maxLines !== void 0 && { maxLines: input.maxLines }
|
|
@@ -36,7 +43,19 @@ const triageBrief = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struc
|
|
|
36
43
|
hasContent: false,
|
|
37
44
|
markdown: "No orientation signal yet — run tests to populate the database."
|
|
38
45
|
};
|
|
39
|
-
})
|
|
46
|
+
});
|
|
47
|
+
/**
|
|
48
|
+
* The Effect-native `triage_brief` tool. The text channel is the
|
|
49
|
+
* pre-rendered `markdown` field itself.
|
|
50
|
+
*
|
|
51
|
+
* @public
|
|
52
|
+
*/
|
|
53
|
+
const triageBriefTool = Tool.make("triage_brief", {
|
|
54
|
+
description: "Use when you need to orient on the current test landscape: failing tests, flaky tests, open TDD sessions, and suggested next actions. Returns markdown in content[] and a typed envelope in structuredContent ({ hasContent, markdown }).",
|
|
55
|
+
parameters: TriageBriefInput,
|
|
56
|
+
success: TriageBriefResult,
|
|
57
|
+
dependencies: [DataReader]
|
|
58
|
+
}).annotate(Tool.Title, "Triage brief").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => encoded.markdown);
|
|
40
59
|
|
|
41
60
|
//#endregion
|
|
42
|
-
export { TriageBriefResult,
|
|
61
|
+
export { TriageBriefInput, TriageBriefResult, handleTriageBrief, triageBriefTool };
|
package/tools/turn-search.js
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RenderText } from "../annotations.js";
|
|
2
2
|
import { Effect, Schema, SchemaGetter } from "effect";
|
|
3
|
-
import { DataReader } from "@vitest-agent/
|
|
3
|
+
import { DataReader } from "@vitest-agent/engine";
|
|
4
|
+
import { Tool } from "effect/unstable/ai";
|
|
4
5
|
|
|
5
6
|
//#region src/tools/turn-search.ts
|
|
6
|
-
/**
|
|
7
|
-
* `turn_search` MCP tool — Schema-driven implementation.
|
|
8
|
-
*
|
|
9
|
-
* @packageDocumentation
|
|
10
|
-
*/
|
|
11
7
|
const TurnRow = Schema.Struct({
|
|
12
8
|
id: Schema.Finite.annotate({ description: "Numeric primary key of this turn row." }),
|
|
13
9
|
sessionId: Schema.Finite.annotate({ description: "Owning `sessions.id` (integer FK)." }),
|
|
@@ -19,6 +15,11 @@ const TurnRow = Schema.Struct({
|
|
|
19
15
|
identifier: "TurnRow",
|
|
20
16
|
description: "One row from the turns log."
|
|
21
17
|
});
|
|
18
|
+
/**
|
|
19
|
+
* The `turn_search` tool's success payload.
|
|
20
|
+
*
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
22
23
|
const TurnSearchResult = Schema.Struct({
|
|
23
24
|
count: Schema.Finite.annotate({ description: "Number of matching turn rows returned." }),
|
|
24
25
|
turns: Schema.Array(TurnRow).annotate({ description: "Matching turns ordered by `occurredAt` ascending." })
|
|
@@ -37,10 +38,15 @@ const TurnSearchAsMarkdown = TurnSearchResult.pipe(Schema.decodeTo(Schema.String
|
|
|
37
38
|
decode: SchemaGetter.transform((data) => formatTurnSearchMarkdown(data)),
|
|
38
39
|
encode: SchemaGetter.forbidden(() => "TurnSearchAsMarkdown is one-way: markdown cannot be parsed back to TurnSearchResult.")
|
|
39
40
|
}));
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
/**
|
|
42
|
+
* The `turn_search` tool's parameters.
|
|
43
|
+
*
|
|
44
|
+
* @public
|
|
45
|
+
*/
|
|
46
|
+
const TurnSearchInput = Schema.Struct({
|
|
47
|
+
sessionId: Schema.optionalKey(Schema.Finite).annotate({ description: "Filter to a specific session id" }),
|
|
48
|
+
since: Schema.optionalKey(Schema.String).annotate({ description: "ISO 8601 cutoff — return turns after this timestamp" }),
|
|
49
|
+
type: Schema.optionalKey(Schema.Literals([
|
|
44
50
|
"user_prompt",
|
|
45
51
|
"tool_call",
|
|
46
52
|
"tool_result",
|
|
@@ -48,9 +54,15 @@ const turnSearch = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct
|
|
|
48
54
|
"hook_fire",
|
|
49
55
|
"note",
|
|
50
56
|
"hypothesis"
|
|
51
|
-
])),
|
|
52
|
-
limit: Schema.
|
|
53
|
-
})
|
|
57
|
+
])).annotate({ description: "Filter by turn type" }),
|
|
58
|
+
limit: Schema.optionalKey(Schema.Finite).annotate({ description: "Max turns to return (default 100)" })
|
|
59
|
+
});
|
|
60
|
+
/**
|
|
61
|
+
* Handler for {@link turnSearchTool}.
|
|
62
|
+
*
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
const handleTurnSearch = (input) => Effect.gen(function* () {
|
|
54
66
|
const rows = yield* (yield* DataReader).searchTurns({
|
|
55
67
|
...input.sessionId !== void 0 && { sessionId: input.sessionId },
|
|
56
68
|
...input.since !== void 0 && { since: input.since },
|
|
@@ -61,7 +73,18 @@ const turnSearch = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struct
|
|
|
61
73
|
count: rows.length,
|
|
62
74
|
turns: rows
|
|
63
75
|
};
|
|
64
|
-
}))
|
|
76
|
+
}).pipe(Effect.orDie);
|
|
77
|
+
/**
|
|
78
|
+
* The Effect-native `turn_search` tool.
|
|
79
|
+
*
|
|
80
|
+
* @public
|
|
81
|
+
*/
|
|
82
|
+
const turnSearchTool = Tool.make("turn_search", {
|
|
83
|
+
description: "Use when you need to find past turns across sessions by type, time, or session. Returns markdown in content[] and a typed JSON object in structuredContent ({ count, turns[] }).",
|
|
84
|
+
parameters: TurnSearchInput,
|
|
85
|
+
success: TurnSearchResult,
|
|
86
|
+
dependencies: [DataReader]
|
|
87
|
+
}).annotate(Tool.Title, "Turn search").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatTurnSearchMarkdown(encoded));
|
|
65
88
|
|
|
66
89
|
//#endregion
|
|
67
|
-
export {
|
|
90
|
+
export { TurnSearchInput, TurnSearchResult, formatTurnSearchMarkdown, handleTurnSearch, turnSearchTool };
|
package/tools/wrapup-prompt.js
CHANGED
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RenderText } from "../annotations.js";
|
|
2
2
|
import { Effect, Schema } from "effect";
|
|
3
|
-
import { formatWrapupEffect } from "@vitest-agent/
|
|
3
|
+
import { DataReader, formatWrapupEffect } from "@vitest-agent/engine";
|
|
4
|
+
import { Tool } from "effect/unstable/ai";
|
|
4
5
|
|
|
5
6
|
//#region src/tools/wrapup-prompt.ts
|
|
6
7
|
/**
|
|
7
|
-
* `wrapup_prompt`
|
|
8
|
+
* The `wrapup_prompt` tool's success payload.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
* markdown rendering with a `hasContent` discriminant for the empty
|
|
11
|
-
* case.
|
|
12
|
-
*
|
|
13
|
-
* @packageDocumentation
|
|
10
|
+
* @public
|
|
14
11
|
*/
|
|
15
12
|
const WrapupPromptResult = Schema.Struct({
|
|
16
13
|
hasContent: Schema.Boolean.annotate({ description: "`false` when there is nothing to wrap up for the named session/kind." }),
|
|
@@ -27,18 +24,29 @@ const WrapupPromptResult = Schema.Struct({
|
|
|
27
24
|
title: "wrapup_prompt result",
|
|
28
25
|
description: "Wrap-up envelope. Branch on `hasContent` for the empty case; consume `markdown` for rendering."
|
|
29
26
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
/**
|
|
28
|
+
* The `wrapup_prompt` tool's parameters.
|
|
29
|
+
*
|
|
30
|
+
* @public
|
|
31
|
+
*/
|
|
32
|
+
const WrapupPromptInput = Schema.Struct({
|
|
33
|
+
sessionId: Schema.optionalKey(Schema.Finite).annotate({ description: "sessions.id (integer); omit to use chatId" }),
|
|
34
|
+
chatId: Schema.optionalKey(Schema.String).annotate({ description: "Host chat UUID (alternative to sessionId)" }),
|
|
35
|
+
kind: Schema.optionalKey(Schema.Literals([
|
|
34
36
|
"stop",
|
|
35
37
|
"session_end",
|
|
36
38
|
"pre_compact",
|
|
37
39
|
"tdd_handoff",
|
|
38
40
|
"user_prompt_nudge"
|
|
39
|
-
])),
|
|
40
|
-
userPromptHint: Schema.
|
|
41
|
-
})
|
|
41
|
+
])).annotate({ description: "Wrap-up flavor (default: session_end)" }),
|
|
42
|
+
userPromptHint: Schema.optionalKey(Schema.String).annotate({ description: "For user_prompt_nudge: the prompt text to inspect" })
|
|
43
|
+
});
|
|
44
|
+
/**
|
|
45
|
+
* Handler for {@link wrapupPromptTool}.
|
|
46
|
+
*
|
|
47
|
+
* @public
|
|
48
|
+
*/
|
|
49
|
+
const handleWrapupPrompt = (input) => Effect.gen(function* () {
|
|
42
50
|
const kind = input.kind ?? "session_end";
|
|
43
51
|
const md = yield* formatWrapupEffect({
|
|
44
52
|
...input.sessionId !== void 0 && { sessionId: input.sessionId },
|
|
@@ -55,7 +63,19 @@ const wrapupPrompt = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Stru
|
|
|
55
63
|
kind,
|
|
56
64
|
markdown: "Nothing to wrap up."
|
|
57
65
|
};
|
|
58
|
-
})
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* The Effect-native `wrapup_prompt` tool. The text channel is the
|
|
69
|
+
* pre-rendered `markdown` field itself.
|
|
70
|
+
*
|
|
71
|
+
* @public
|
|
72
|
+
*/
|
|
73
|
+
const wrapupPromptTool = Tool.make("wrapup_prompt", {
|
|
74
|
+
description: "Use when a session is ending and you need a tailored wrap-up prompt (Stop / SessionEnd / PreCompact / TDD handoff / UserPromptSubmit nudge variants). Returns markdown in content[] and a typed envelope in structuredContent ({ hasContent, kind, markdown }).",
|
|
75
|
+
parameters: WrapupPromptInput,
|
|
76
|
+
success: WrapupPromptResult,
|
|
77
|
+
dependencies: [DataReader]
|
|
78
|
+
}).annotate(Tool.Title, "Wrap-up prompt").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => encoded.markdown);
|
|
59
79
|
|
|
60
80
|
//#endregion
|
|
61
|
-
export { WrapupPromptResult,
|
|
81
|
+
export { WrapupPromptInput, WrapupPromptResult, handleWrapupPrompt, wrapupPromptTool };
|