@vitest-agent/mcp 3.0.4 → 4.0.1
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
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RenderText } from "../annotations.js";
|
|
2
2
|
import { Effect, Option, 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/failure-signature-get.ts
|
|
6
|
-
/**
|
|
7
|
-
* `failure_signature_get` MCP tool — Schema-driven implementation.
|
|
8
|
-
*
|
|
9
|
-
* @packageDocumentation
|
|
10
|
-
*/
|
|
11
7
|
const RecentError = Schema.Struct({
|
|
12
8
|
runId: Schema.Number,
|
|
13
9
|
errorName: Schema.NullOr(Schema.String),
|
|
@@ -29,6 +25,11 @@ const SignatureMissing = Schema.Struct({
|
|
|
29
25
|
found: Schema.Literal(false),
|
|
30
26
|
requestedHash: Schema.String
|
|
31
27
|
});
|
|
28
|
+
/**
|
|
29
|
+
* The `failure_signature_get` tool's success payload.
|
|
30
|
+
*
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
32
33
|
const FailureSignatureGetResult = Schema.Union([SignatureFound, SignatureMissing]).annotate({
|
|
33
34
|
identifier: "FailureSignatureGetResult",
|
|
34
35
|
title: "failure_signature_get result",
|
|
@@ -56,7 +57,18 @@ const FailureSignatureGetAsMarkdown = FailureSignatureGetResult.pipe(Schema.deco
|
|
|
56
57
|
decode: SchemaGetter.transform((data) => formatFailureSignatureMarkdown(data)),
|
|
57
58
|
encode: SchemaGetter.forbidden(() => "FailureSignatureGetAsMarkdown is one-way.")
|
|
58
59
|
}));
|
|
59
|
-
|
|
60
|
+
/**
|
|
61
|
+
* The `failure_signature_get` tool's parameters.
|
|
62
|
+
*
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
const FailureSignatureGetInput = Schema.Struct({ hash: Schema.String.annotate({ description: "16-char failure signature hash" }) });
|
|
66
|
+
/**
|
|
67
|
+
* Handler for {@link failureSignatureGetTool}.
|
|
68
|
+
*
|
|
69
|
+
* @public
|
|
70
|
+
*/
|
|
71
|
+
const handleFailureSignatureGet = (input) => Effect.gen(function* () {
|
|
60
72
|
const opt = yield* (yield* DataReader).getFailureSignatureByHash(input.hash);
|
|
61
73
|
if (Option.isNone(opt)) return {
|
|
62
74
|
found: false,
|
|
@@ -66,7 +78,18 @@ const failureSignatureGet = publicProcedure.input(Schema.toStandardSchemaV1(Sche
|
|
|
66
78
|
found: true,
|
|
67
79
|
...opt.value
|
|
68
80
|
};
|
|
69
|
-
}))
|
|
81
|
+
}).pipe(Effect.orDie);
|
|
82
|
+
/**
|
|
83
|
+
* The Effect-native `failure_signature_get` tool.
|
|
84
|
+
*
|
|
85
|
+
* @public
|
|
86
|
+
*/
|
|
87
|
+
const failureSignatureGetTool = Tool.make("failure_signature_get", {
|
|
88
|
+
description: "Use when you have a failure-signature hash and need its first-seen date and occurrence history. Returns markdown in content[] and a typed JSON object in structuredContent ({ found, signatureHash?, firstSeenAt?, occurrenceCount?, recentErrors?[] } or absent variant).",
|
|
89
|
+
parameters: FailureSignatureGetInput,
|
|
90
|
+
success: FailureSignatureGetResult,
|
|
91
|
+
dependencies: [DataReader]
|
|
92
|
+
}).annotate(Tool.Title, "Failure signature").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatFailureSignatureMarkdown(encoded));
|
|
70
93
|
|
|
71
94
|
//#endregion
|
|
72
|
-
export {
|
|
95
|
+
export { FailureSignatureGetInput, FailureSignatureGetResult, failureSignatureGetTool, formatFailureSignatureMarkdown, handleFailureSignatureGet };
|
package/tools/file-coverage.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RenderText } from "../annotations.js";
|
|
2
2
|
import { Effect, Option, Schema, SchemaGetter } from "effect";
|
|
3
|
-
import {
|
|
3
|
+
import { DataReader } from "@vitest-agent/engine";
|
|
4
|
+
import { Tool } from "effect/unstable/ai";
|
|
5
|
+
import { CoverageTotals, FileCoverageReport } from "@vitest-agent/sdk";
|
|
4
6
|
|
|
5
7
|
//#region src/tools/file-coverage.ts
|
|
6
|
-
/**
|
|
7
|
-
* `file_coverage` MCP tool — Schema-driven implementation.
|
|
8
|
-
*
|
|
9
|
-
* @packageDocumentation
|
|
10
|
-
*/
|
|
11
8
|
const CoverageGlobalThresholds = Schema.Struct({
|
|
12
9
|
statements: Schema.optional(Schema.Number),
|
|
13
10
|
branches: Schema.optional(Schema.Number),
|
|
@@ -33,6 +30,11 @@ const FileCoverageAbsent = Schema.Struct({
|
|
|
33
30
|
dataAvailable: Schema.Literal(false),
|
|
34
31
|
filePath: Schema.String
|
|
35
32
|
}).annotate({ identifier: "FileCoverageAbsent" });
|
|
33
|
+
/**
|
|
34
|
+
* The `file_coverage` tool's success payload.
|
|
35
|
+
*
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
36
38
|
const FileCoverageResult = Schema.Union([
|
|
37
39
|
FileCoverageMatched,
|
|
38
40
|
FileCoverageNoMatch,
|
|
@@ -73,10 +75,21 @@ const FileCoverageAsMarkdown = FileCoverageResult.pipe(Schema.decodeTo(Schema.St
|
|
|
73
75
|
decode: SchemaGetter.transform((data) => formatFileCoverageMarkdown(data)),
|
|
74
76
|
encode: SchemaGetter.forbidden(() => "FileCoverageAsMarkdown is one-way.")
|
|
75
77
|
}));
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
/**
|
|
79
|
+
* The `file_coverage` tool's parameters.
|
|
80
|
+
*
|
|
81
|
+
* @public
|
|
82
|
+
*/
|
|
83
|
+
const FileCoverageInput = Schema.Struct({
|
|
84
|
+
filePath: Schema.String.annotate({ description: "Source file path to check coverage for" }),
|
|
85
|
+
project: Schema.optionalKey(Schema.String).annotate({ description: "Project name" })
|
|
86
|
+
});
|
|
87
|
+
/**
|
|
88
|
+
* Handler for {@link fileCoverageTool}.
|
|
89
|
+
*
|
|
90
|
+
* @public
|
|
91
|
+
*/
|
|
92
|
+
const handleFileCoverage = (input) => Effect.gen(function* () {
|
|
80
93
|
const reader = yield* DataReader;
|
|
81
94
|
const project = input.project ?? "default";
|
|
82
95
|
const coverageOpt = yield* reader.getCoverage(project);
|
|
@@ -103,7 +116,18 @@ const fileCoverage = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Stru
|
|
|
103
116
|
totals: coverage.totals,
|
|
104
117
|
relatedTestFiles
|
|
105
118
|
};
|
|
106
|
-
}))
|
|
119
|
+
}).pipe(Effect.orDie);
|
|
120
|
+
/**
|
|
121
|
+
* The Effect-native `file_coverage` tool.
|
|
122
|
+
*
|
|
123
|
+
* @public
|
|
124
|
+
*/
|
|
125
|
+
const fileCoverageTool = Tool.make("file_coverage", {
|
|
126
|
+
description: "Use when you need coverage for one source file: per-metric values, uncovered lines, and related tests. Returns markdown in content[] and a typed JSON object in structuredContent ({ dataAvailable, matched?, filePath, report?, totals?, relatedTestFiles[] }).",
|
|
127
|
+
parameters: FileCoverageInput,
|
|
128
|
+
success: FileCoverageResult,
|
|
129
|
+
dependencies: [DataReader]
|
|
130
|
+
}).annotate(Tool.Title, "File coverage").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatFileCoverageMarkdown(encoded));
|
|
107
131
|
|
|
108
132
|
//#endregion
|
|
109
|
-
export {
|
|
133
|
+
export { FileCoverageInput, FileCoverageResult, fileCoverageTool, formatFileCoverageMarkdown, handleFileCoverage };
|
package/tools/help.js
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Schema } from "effect";
|
|
1
|
+
import { RenderText } from "../annotations.js";
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
3
|
+
import { Tool } from "effect/unstable/ai";
|
|
3
4
|
|
|
4
5
|
//#region src/tools/help.ts
|
|
6
|
+
/**
|
|
7
|
+
* The `help` tool's success payload.
|
|
8
|
+
*
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
5
11
|
const HelpResult = Schema.Struct({ helpText: Schema.String.annotate({ description: "Markdown table of every MCP tool with parameters and a one-line description." }) }).annotate({
|
|
6
12
|
identifier: "HelpResult",
|
|
7
13
|
title: "help result",
|
|
8
14
|
description: "Static help reference. Read structuredContent.helpText programmatically; the same string lives in content[].text for transcripts."
|
|
9
15
|
});
|
|
16
|
+
/**
|
|
17
|
+
* The static markdown `help` returns. Exported so `__test__/help-drift.test.ts`
|
|
18
|
+
* can pin its tool and prompt rows to the served toolkit and prompt layer.
|
|
19
|
+
*
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
10
22
|
const HELP_TEXT = `# vitest-agent MCP Tools
|
|
11
23
|
|
|
12
24
|
> Consolidated tool surface (Phase 3 of the agent-agnostic taxonomy).
|
|
@@ -135,6 +147,19 @@ const HELP_TEXT = `# vitest-agent MCP Tools
|
|
|
135
147
|
- \`{ action: "list_by_goal", goalId }\`
|
|
136
148
|
- \`{ action: "list_by_tdd_task", tddTaskId }\`
|
|
137
149
|
|
|
150
|
+
## Prompts
|
|
151
|
+
|
|
152
|
+
Six framing-only prompts (\`prompts/get\`; Claude Code surfaces them as slash commands):
|
|
153
|
+
|
|
154
|
+
| Prompt | Arguments | Description |
|
|
155
|
+
| ------ | --------- | ----------- |
|
|
156
|
+
| \`triage\` | \`project?\` | Orient toward a triage workflow over the most recent run |
|
|
157
|
+
| \`why-flaky\` | \`test\`, \`project?\` | Diagnose why a named test is flaky |
|
|
158
|
+
| \`regression-since-pass\` | \`test\`, \`project?\` | Walk back from the last passing run to the change that broke it |
|
|
159
|
+
| \`explain-failure\` | \`signature\` | Root-cause explanation from a failure signature's recurrence history |
|
|
160
|
+
| \`tdd-resume\` | \`sessionId?\` | Resume the active TDD task from its current phase |
|
|
161
|
+
| \`wrapup\` | \`kind?\`, \`since?\` | The same wrap-up content the post-hooks emit automatically |
|
|
162
|
+
|
|
138
163
|
## Parameter Key
|
|
139
164
|
|
|
140
165
|
- **Required** parameters are unmarked
|
|
@@ -143,7 +168,23 @@ const HELP_TEXT = `# vitest-agent MCP Tools
|
|
|
143
168
|
- \`state\` accepts: \`passed\`, \`failed\`, \`skipped\`, \`pending\`
|
|
144
169
|
- \`scope\` accepts: \`global\`, \`project\`, \`module\`, \`suite\`, \`test\`, \`note\`
|
|
145
170
|
`;
|
|
146
|
-
|
|
171
|
+
/**
|
|
172
|
+
* The Effect-native `help` tool. Renders the help markdown as the text
|
|
173
|
+
* channel via `RenderText` while `structuredContent.helpText` carries the
|
|
174
|
+
* same string for programmatic readers.
|
|
175
|
+
*
|
|
176
|
+
* @public
|
|
177
|
+
*/
|
|
178
|
+
const helpTool = Tool.make("help", {
|
|
179
|
+
description: "List all available MCP tools with parameters. Read structuredContent.helpText programmatically; the same markdown lives in content[].text.",
|
|
180
|
+
success: HelpResult
|
|
181
|
+
}).annotate(Tool.Title, "Help").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => encoded.helpText);
|
|
182
|
+
/**
|
|
183
|
+
* Handler for {@link helpTool}.
|
|
184
|
+
*
|
|
185
|
+
* @public
|
|
186
|
+
*/
|
|
187
|
+
const handleHelp = () => Effect.succeed({ helpText: HELP_TEXT });
|
|
147
188
|
|
|
148
189
|
//#endregion
|
|
149
|
-
export { HelpResult,
|
|
190
|
+
export { HELP_TEXT, HelpResult, handleHelp, helpTool };
|
package/tools/history.js
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RenderText } from "../annotations.js";
|
|
2
2
|
import { Effect, Schema, SchemaGetter } from "effect";
|
|
3
|
-
import { DataReader
|
|
3
|
+
import { DataReader } from "@vitest-agent/engine";
|
|
4
|
+
import { Tool } from "effect/unstable/ai";
|
|
5
|
+
import { HistoryRecord } from "@vitest-agent/sdk";
|
|
4
6
|
|
|
5
7
|
//#region src/tools/history.ts
|
|
6
|
-
/**
|
|
7
|
-
* `test_history` MCP tool — Schema-driven implementation.
|
|
8
|
-
*
|
|
9
|
-
* The structured payload bundles the underlying `HistoryRecord` plus
|
|
10
|
-
* the lighter `flaky` / `persistent` projections the UI uses, so an
|
|
11
|
-
* agent doesn't need to recompute them from runs[].
|
|
12
|
-
*
|
|
13
|
-
* @packageDocumentation
|
|
14
|
-
*/
|
|
15
8
|
const FlakyTestRow = Schema.Struct({
|
|
16
9
|
fullName: Schema.String.annotate({ description: "Full hierarchical test name (`describe > it`)." }),
|
|
17
10
|
modulePath: Schema.String.annotate({ description: "Project-relative test module path -- disambiguates same-named tests across files." }),
|
|
@@ -44,6 +37,11 @@ const RecoveredTestRow = Schema.Struct({
|
|
|
44
37
|
identifier: "RecoveredTestRow",
|
|
45
38
|
description: "A test whose latest run passed after the previous one failed."
|
|
46
39
|
});
|
|
40
|
+
/**
|
|
41
|
+
* The `test_history` tool's success payload.
|
|
42
|
+
*
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
47
45
|
const TestHistoryResult = Schema.Struct({
|
|
48
46
|
project: Schema.String.annotate({ description: "Workspace project key the history was computed for." }),
|
|
49
47
|
hasData: Schema.Boolean.annotate({ description: "`false` when no history rows exist for the project — agent should suggest running tests first." }),
|
|
@@ -91,12 +89,23 @@ const TestHistoryAsMarkdown = TestHistoryResult.pipe(Schema.decodeTo(Schema.Stri
|
|
|
91
89
|
decode: SchemaGetter.transform((data) => formatTestHistoryMarkdown(data)),
|
|
92
90
|
encode: SchemaGetter.forbidden(() => "TestHistoryAsMarkdown is one-way: markdown cannot be parsed back to TestHistoryResult.")
|
|
93
91
|
}));
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
92
|
+
/**
|
|
93
|
+
* The `test_history` tool's parameters.
|
|
94
|
+
*
|
|
95
|
+
* @public
|
|
96
|
+
*/
|
|
97
|
+
const TestHistoryInput = Schema.Struct({
|
|
98
|
+
project: Schema.String.annotate({ description: "Project name (required)" }),
|
|
99
|
+
testName: Schema.optionalKey(Schema.String).annotate({ description: "Exact full_name match — narrows to a single test's history." }),
|
|
100
|
+
modulePath: Schema.optionalKey(Schema.String).annotate({ description: "Exact module_path match — narrows to tests in one file." }),
|
|
101
|
+
limit: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThan(0)).annotate({ description: "Max runs kept per test, most-recent-first. Must be a positive integer. Default 20." }))
|
|
102
|
+
});
|
|
103
|
+
/**
|
|
104
|
+
* Handler for {@link testHistoryTool}.
|
|
105
|
+
*
|
|
106
|
+
* @public
|
|
107
|
+
*/
|
|
108
|
+
const handleTestHistory = (input) => Effect.gen(function* () {
|
|
100
109
|
const reader = yield* DataReader;
|
|
101
110
|
const scopeOptions = {
|
|
102
111
|
...input.testName !== void 0 && { testName: input.testName },
|
|
@@ -131,7 +140,18 @@ const testHistory = publicProcedure.input(Schema.toStandardSchemaV1(Schema.Struc
|
|
|
131
140
|
persistent,
|
|
132
141
|
recovered
|
|
133
142
|
};
|
|
134
|
-
}))
|
|
143
|
+
}).pipe(Effect.orDie);
|
|
144
|
+
/**
|
|
145
|
+
* The Effect-native `test_history` tool.
|
|
146
|
+
*
|
|
147
|
+
* @public
|
|
148
|
+
*/
|
|
149
|
+
const testHistoryTool = Tool.make("test_history", {
|
|
150
|
+
description: "Use when failures recur and you need flaky, persistent, and recovered test classifications. Returns markdown in content[] and a typed JSON object in structuredContent (project, hasData, history, flaky[], persistent[], recovered[]). Optional testName/modulePath narrow to a single test; limit caps runs kept per test (default 20) — omit all three only when you actually need the whole project's history.",
|
|
151
|
+
parameters: TestHistoryInput,
|
|
152
|
+
success: TestHistoryResult,
|
|
153
|
+
dependencies: [DataReader]
|
|
154
|
+
}).annotate(Tool.Title, "Test history").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, true).annotate(RenderText, (encoded) => formatTestHistoryMarkdown(encoded));
|
|
135
155
|
|
|
136
156
|
//#endregion
|
|
137
|
-
export {
|
|
157
|
+
export { TestHistoryInput, TestHistoryResult, formatTestHistoryMarkdown, handleTestHistory, testHistoryTool };
|
package/tools/hypothesis.js
CHANGED
|
@@ -1,18 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RenderText } from "../annotations.js";
|
|
2
|
+
import { McpSession } from "../session.js";
|
|
3
|
+
import { IdempotentReplayMarker } from "../utils/replay-marker.js";
|
|
2
4
|
import { Effect, Match, Option, Schema } from "effect";
|
|
3
|
-
import { DataReader, DataStore
|
|
5
|
+
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
6
|
+
import { Tool } from "effect/unstable/ai";
|
|
7
|
+
import { DataStoreError } from "@vitest-agent/sdk";
|
|
4
8
|
|
|
5
9
|
//#region src/tools/hypothesis.ts
|
|
6
|
-
/**
|
|
7
|
-
* Consolidated `hypothesis` MCP tool — Schema-driven implementation.
|
|
8
|
-
*
|
|
9
|
-
* `record` and `validate` are mutations whose result is a small
|
|
10
|
-
* structured envelope. `list` now returns a structured array; the
|
|
11
|
-
* boundary in server.ts renders it as markdown via the exported
|
|
12
|
-
* `formatHypothesisListMarkdown` helper.
|
|
13
|
-
*
|
|
14
|
-
* @packageDocumentation
|
|
15
|
-
*/
|
|
16
10
|
const HypothesisRowSchema = Schema.Struct({
|
|
17
11
|
id: Schema.Number,
|
|
18
12
|
sessionId: Schema.Number,
|
|
@@ -30,7 +24,10 @@ const HypothesisRecordOk = Schema.Struct({
|
|
|
30
24
|
action: Schema.Literal("record"),
|
|
31
25
|
id: Schema.Finite.annotate({ description: "Newly inserted hypothesis row primary key." })
|
|
32
26
|
});
|
|
33
|
-
const HypothesisValidateOk = Schema.Struct({
|
|
27
|
+
const HypothesisValidateOk = Schema.Struct({
|
|
28
|
+
action: Schema.Literal("validate"),
|
|
29
|
+
...IdempotentReplayMarker
|
|
30
|
+
});
|
|
34
31
|
const HypothesisListOk = Schema.Struct({
|
|
35
32
|
action: Schema.Literal("list"),
|
|
36
33
|
count: Schema.Number,
|
|
@@ -55,117 +52,136 @@ const formatHypothesisListMarkdown = (data) => {
|
|
|
55
52
|
}
|
|
56
53
|
return lines.join("\n");
|
|
57
54
|
};
|
|
55
|
+
/** Number-or-numeric-string id: LLM orchestrators routinely stringify numeric tool inputs. */
|
|
56
|
+
const CoercibleId = Schema.Union([Schema.Finite, Schema.FiniteFromString]);
|
|
57
|
+
/** The text channel: `list` renders markdown; `record` / `validate` render the pretty-printed JSON. */
|
|
58
|
+
const renderHypothesisText = (data) => data.action === "list" ? formatHypothesisListMarkdown(data) : JSON.stringify(data, null, 2);
|
|
58
59
|
const RecordVariant = Schema.Struct({
|
|
59
|
-
action: Schema.Literal("record"),
|
|
60
|
-
tddTaskId: Schema.
|
|
61
|
-
sessionId: Schema.
|
|
62
|
-
content: Schema.String,
|
|
63
|
-
createdTurnId: Schema.
|
|
64
|
-
citedTestErrorId: Schema.
|
|
65
|
-
citedStackFrameId: Schema.
|
|
60
|
+
action: Schema.Literal("record").annotate({ description: "CRUD discriminator" }),
|
|
61
|
+
tddTaskId: Schema.optionalKey(CoercibleId).annotate({ description: "record: tdd task id returned by tdd_task action='start' — binds the hypothesis to that task's session deterministically" }),
|
|
62
|
+
sessionId: Schema.optionalKey(CoercibleId).annotate({ description: "list: filter by session id. record: dev/test fallback only — ignored when host context is recovered; never pass a tddTaskId value here" }),
|
|
63
|
+
content: Schema.String.annotate({ description: "Hypothesis content (action=record)" }),
|
|
64
|
+
createdTurnId: Schema.optionalKey(Schema.Finite),
|
|
65
|
+
citedTestErrorId: Schema.optionalKey(Schema.Finite),
|
|
66
|
+
citedStackFrameId: Schema.optionalKey(Schema.Finite)
|
|
66
67
|
});
|
|
67
68
|
const ValidateVariant = Schema.Struct({
|
|
68
|
-
action: Schema.Literal("validate"),
|
|
69
|
-
id: Schema.
|
|
69
|
+
action: Schema.Literal("validate").annotate({ description: "CRUD discriminator" }),
|
|
70
|
+
id: Schema.Finite.annotate({ description: "Hypothesis id (action=validate)" }),
|
|
70
71
|
outcome: Schema.Literals([
|
|
71
72
|
"confirmed",
|
|
72
73
|
"refuted",
|
|
73
74
|
"abandoned"
|
|
74
|
-
]),
|
|
75
|
-
validatedTurnId: Schema.
|
|
76
|
-
validatedAt: Schema.
|
|
75
|
+
]).annotate({ description: "validate: 'confirmed'|'refuted'|'abandoned'; list filter may include 'open'" }),
|
|
76
|
+
validatedTurnId: Schema.optionalKey(Schema.Finite),
|
|
77
|
+
validatedAt: Schema.optionalKey(Schema.String).annotate({ description: "ISO 8601 timestamp (action=validate)" })
|
|
77
78
|
});
|
|
78
79
|
const ListVariant = Schema.Struct({
|
|
79
|
-
action: Schema.Literal("list"),
|
|
80
|
-
sessionId: Schema.
|
|
81
|
-
outcome: Schema.
|
|
80
|
+
action: Schema.Literal("list").annotate({ description: "CRUD discriminator" }),
|
|
81
|
+
sessionId: Schema.optionalKey(Schema.Finite).annotate({ description: "list: filter by session id. record: dev/test fallback only — ignored when host context is recovered; never pass a tddTaskId value here" }),
|
|
82
|
+
outcome: Schema.optionalKey(Schema.Literals([
|
|
82
83
|
"confirmed",
|
|
83
84
|
"refuted",
|
|
84
85
|
"abandoned",
|
|
85
86
|
"open"
|
|
86
|
-
])),
|
|
87
|
-
limit: Schema.
|
|
87
|
+
])).annotate({ description: "validate: 'confirmed'|'refuted'|'abandoned'; list filter may include 'open'" }),
|
|
88
|
+
limit: Schema.optionalKey(Schema.Finite)
|
|
88
89
|
});
|
|
90
|
+
/**
|
|
91
|
+
* The `hypothesis` tool's parameters — a union discriminated on `action`.
|
|
92
|
+
*
|
|
93
|
+
* @public
|
|
94
|
+
*/
|
|
89
95
|
const HypothesisInput = Schema.Union([
|
|
90
96
|
RecordVariant,
|
|
91
97
|
ValidateVariant,
|
|
92
98
|
ListVariant
|
|
93
99
|
]);
|
|
94
100
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
101
|
+
* Handler for {@link hypothesisTool}. `record` fails (as a defect, so it
|
|
102
|
+
* reaches the agent as the `UnexpectedToolError` envelope) when the
|
|
103
|
+
* binding session cannot be resolved.
|
|
104
|
+
*
|
|
105
|
+
* @public
|
|
99
106
|
*/
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (sc !== null) {
|
|
123
|
-
const main = yield* reader.getSessionByChatId(sc.chatId);
|
|
124
|
-
if (Option.isSome(main)) {
|
|
125
|
-
const sub = yield* reader.findActiveSubagentSession(main.value.id);
|
|
126
|
-
resolvedSessionId = Option.isSome(sub) ? sub.value.id : main.value.id;
|
|
127
|
-
}
|
|
107
|
+
const handleHypothesis = (input) => Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
|
|
108
|
+
record: (variant) => Effect.gen(function* () {
|
|
109
|
+
const store = yield* DataStore;
|
|
110
|
+
const reader = yield* DataReader;
|
|
111
|
+
const session = yield* McpSession;
|
|
112
|
+
let resolvedSessionId;
|
|
113
|
+
if (variant.tddTaskId !== void 0) {
|
|
114
|
+
const bound = yield* reader.getSessionByTddTaskId(variant.tddTaskId);
|
|
115
|
+
if (Option.isNone(bound)) return yield* Effect.fail(new DataStoreError({
|
|
116
|
+
operation: "write",
|
|
117
|
+
table: "hypotheses",
|
|
118
|
+
reason: `unknown tddTaskId ${variant.tddTaskId}: no session found to attribute hypothesis`
|
|
119
|
+
}));
|
|
120
|
+
resolvedSessionId = bound.value.id;
|
|
121
|
+
} else {
|
|
122
|
+
const sc = session.sessionContext.get();
|
|
123
|
+
resolvedSessionId = variant.sessionId;
|
|
124
|
+
if (sc !== null) {
|
|
125
|
+
const main = yield* reader.getSessionByChatId(sc.chatId);
|
|
126
|
+
if (Option.isSome(main)) {
|
|
127
|
+
const sub = yield* reader.findActiveSubagentSession(main.value.id);
|
|
128
|
+
resolvedSessionId = Option.isSome(sub) ? sub.value.id : main.value.id;
|
|
128
129
|
}
|
|
129
|
-
if (resolvedSessionId === void 0) return yield* Effect.fail(new DataStoreError({
|
|
130
|
-
operation: "write",
|
|
131
|
-
table: "hypotheses",
|
|
132
|
-
reason: "no recovered session context: pass tddTaskId (the id returned by tdd_task action:start) to bind this hypothesis to your task's session — do not retry with a raw sessionId, and never pass tddTaskId under a sessionId key"
|
|
133
|
-
}));
|
|
134
130
|
}
|
|
135
|
-
return {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
|
|
131
|
+
if (resolvedSessionId === void 0) return yield* Effect.fail(new DataStoreError({
|
|
132
|
+
operation: "write",
|
|
133
|
+
table: "hypotheses",
|
|
134
|
+
reason: "no recovered session context: pass tddTaskId (the id returned by tdd_task action:start) to bind this hypothesis to your task's session — do not retry with a raw sessionId, and never pass tddTaskId under a sessionId key"
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
action: "record",
|
|
139
|
+
id: yield* store.writeHypothesis({
|
|
140
|
+
sessionId: resolvedSessionId,
|
|
141
|
+
content: variant.content,
|
|
142
|
+
...variant.createdTurnId !== void 0 && { createdTurnId: variant.createdTurnId },
|
|
143
|
+
...variant.citedTestErrorId !== void 0 && { citedTestErrorId: variant.citedTestErrorId },
|
|
144
|
+
...variant.citedStackFrameId !== void 0 && { citedStackFrameId: variant.citedStackFrameId }
|
|
145
|
+
})
|
|
146
|
+
};
|
|
147
|
+
}),
|
|
148
|
+
validate: (variant) => Effect.gen(function* () {
|
|
149
|
+
yield* (yield* DataStore).validateHypothesis({
|
|
150
|
+
id: variant.id,
|
|
151
|
+
outcome: variant.outcome,
|
|
152
|
+
validatedAt: variant.validatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
153
|
+
...variant.validatedTurnId !== void 0 && { validatedTurnId: variant.validatedTurnId }
|
|
154
|
+
});
|
|
155
|
+
return { action: "validate" };
|
|
156
|
+
}),
|
|
157
|
+
list: (variant) => Effect.gen(function* () {
|
|
158
|
+
const rows = yield* (yield* DataReader).listHypotheses({
|
|
159
|
+
...variant.sessionId !== void 0 && { sessionId: variant.sessionId },
|
|
160
|
+
...variant.outcome !== void 0 && { outcome: variant.outcome },
|
|
161
|
+
...variant.limit !== void 0 && { limit: variant.limit }
|
|
162
|
+
});
|
|
163
|
+
return {
|
|
164
|
+
action: "list",
|
|
165
|
+
count: rows.length,
|
|
166
|
+
hypotheses: rows
|
|
167
|
+
};
|
|
168
|
+
})
|
|
169
|
+
})).pipe(Effect.orDie);
|
|
170
|
+
/**
|
|
171
|
+
* The Effect-native `hypothesis` tool.
|
|
172
|
+
*
|
|
173
|
+
* @public
|
|
174
|
+
*/
|
|
175
|
+
const hypothesisTool = Tool.make("hypothesis", {
|
|
176
|
+
description: "Use to manage debugging hypotheses, with a CRUD action discriminator: action='record' (content, tddTaskId?, optional citation ids) writes a hypothesis — the binding session is resolved server-side from the recovered host context (active TDD subagent, else main session); pass tddTaskId (returned by tdd_task action='start') to bind deterministically to that task's session, and do not pass sessionId when recording; action='validate' (id, outcome, validatedAt?) records a validation outcome — validatedAt is optional and defaults server-side to now when omitted, or is honored verbatim when supplied; action='list' (sessionId?, outcome?, limit?) returns matching hypotheses as markdown.",
|
|
177
|
+
parameters: HypothesisInput,
|
|
178
|
+
success: HypothesisResult,
|
|
179
|
+
dependencies: [
|
|
180
|
+
DataReader,
|
|
181
|
+
DataStore,
|
|
182
|
+
McpSession
|
|
183
|
+
]
|
|
184
|
+
}).annotate(Tool.Title, "Hypothesis").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.OpenWorld, false).annotate(Tool.Idempotent, false).annotate(RenderText, (encoded) => renderHypothesisText(encoded));
|
|
169
185
|
|
|
170
186
|
//#endregion
|
|
171
|
-
export {
|
|
187
|
+
export { HypothesisInput, HypothesisResult, formatHypothesisListMarkdown, handleHypothesis, hypothesisTool };
|