@vitest-agent/mcp 5.3.1 → 6.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 +1 -1
- package/index.d.ts +3101 -450
- package/index.js +31 -28
- package/main.js +8 -44
- package/package.json +9 -8
- package/replay.js +93 -0
- package/server.js +1 -1
- package/toolkit.js +25 -9
- package/tools/_tdd-error-envelope.js +119 -70
- package/tools/acceptance-metrics.js +1 -1
- package/tools/cache-health.js +1 -1
- package/tools/commit-changes.js +1 -1
- package/tools/configure.js +1 -1
- package/tools/coverage.js +1 -1
- package/tools/errors.js +1 -1
- package/tools/failure-signature-get.js +1 -1
- package/tools/file-coverage.js +1 -1
- package/tools/history.js +1 -1
- package/tools/hypothesis.js +7 -2
- package/tools/inventory.js +1 -1
- package/tools/note.js +1 -1
- package/tools/overview.js +1 -1
- package/tools/register-agent.js +2 -2
- package/tools/run-tests.js +310 -252
- package/tools/settings-list.js +1 -1
- package/tools/status.js +1 -1
- package/tools/tdd-artifact.js +1 -1
- package/tools/tdd-behavior.js +28 -23
- package/tools/tdd-goal.js +27 -24
- package/tools/tdd-phase-transition-request.js +1 -1
- package/tools/tdd-progress-push.js +1 -1
- package/tools/tdd-task.js +7 -2
- package/tools/test.js +1 -1
- package/tools/trends.js +1 -1
- package/tools/triage-brief.js +1 -1
- package/tools/turn-search.js +1 -1
- package/tools/wrapup-prompt.js +1 -1
- package/utils/safe-format-fatal-error.js +1 -1
- package/version.js +1 -1
- package/idempotency.js +0 -123
package/tools/settings-list.js
CHANGED
package/tools/status.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ToolOutputSchema } from "@effected/mcp";
|
|
2
2
|
import { Effect, Option, Schema } from "effect";
|
|
3
|
-
import { DataReader } from "@vitest-agent/engine";
|
|
4
3
|
import { Tool } from "effect/ai";
|
|
4
|
+
import { DataReader } from "@vitest-agent/engine";
|
|
5
5
|
import { CacheManifestEntry, StrayOutput } from "@vitest-agent/sdk";
|
|
6
6
|
|
|
7
7
|
// src/tools/status.ts
|
package/tools/tdd-artifact.js
CHANGED
package/tools/tdd-behavior.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { IdempotentReplayMarker } from "../utils/replay-marker.js";
|
|
2
|
-
import {
|
|
3
|
-
import { McpToolkit,
|
|
2
|
+
import { behaviorNotFound, goalNotFound, illegalStatusTransition, tddEnvelope, tddTaskAlreadyEnded, tddTaskNotFound } from "./_tdd-error-envelope.js";
|
|
3
|
+
import { McpToolkit, ToolOutcome, ToolRefusal } from "@effected/mcp";
|
|
4
4
|
import { Effect, Match, Option, Schema } from "effect";
|
|
5
|
-
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
6
5
|
import { Tool } from "effect/ai";
|
|
6
|
+
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
7
7
|
import { BehaviorDetail, BehaviorRow } from "@vitest-agent/sdk";
|
|
8
|
-
import { Remediation } from "@effected/engine";
|
|
9
8
|
|
|
10
9
|
// src/tools/tdd-behavior.ts
|
|
11
10
|
/**
|
|
@@ -23,13 +22,6 @@ const BehaviorStatus = Schema.Literals([
|
|
|
23
22
|
"done",
|
|
24
23
|
"abandoned"
|
|
25
24
|
]);
|
|
26
|
-
const TddBehaviorErrorEnvelope = Schema.Struct({
|
|
27
|
-
ok: Schema.Literal(false).annotate({ description: "Discriminant — `false` when a tagged TDD error was caught." }),
|
|
28
|
-
error: Schema.StructWithRest(Schema.Struct({
|
|
29
|
-
_tag: Schema.String.annotate({ description: "Tagged error name (e.g. BehaviorNotFoundError, GoalNotFoundError)." }),
|
|
30
|
-
remediation: Remediation.annotate({ description: "Suggested next action: a plain-language hint, the tool to call, and its arguments." })
|
|
31
|
-
}), [Schema.Record(Schema.String, Schema.Unknown)])
|
|
32
|
-
}).annotate({ identifier: "TddBehaviorErrorEnvelope" });
|
|
33
25
|
const TddBehaviorCreateOk = Schema.Struct({
|
|
34
26
|
ok: Schema.Literal(true),
|
|
35
27
|
action: Schema.Literal("create"),
|
|
@@ -68,16 +60,20 @@ const TddBehaviorListByTddTaskOk = Schema.Struct({
|
|
|
68
60
|
tddTaskId: Schema.Number,
|
|
69
61
|
behaviors: Schema.Array(BehaviorRow)
|
|
70
62
|
});
|
|
71
|
-
|
|
63
|
+
/**
|
|
64
|
+
* The `tdd_behavior` tool's success payload.
|
|
65
|
+
*
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
68
|
+
const TddBehaviorResult = tddEnvelope(Schema.Union([
|
|
72
69
|
TddBehaviorCreateOk,
|
|
73
70
|
TddBehaviorUpdateOk,
|
|
74
71
|
TddBehaviorDeleteOk,
|
|
75
72
|
TddBehaviorGetFound,
|
|
76
73
|
TddBehaviorGetMissing,
|
|
77
74
|
TddBehaviorListByGoalOk,
|
|
78
|
-
TddBehaviorListByTddTaskOk
|
|
79
|
-
|
|
80
|
-
])).annotate({
|
|
75
|
+
TddBehaviorListByTddTaskOk
|
|
76
|
+
]), "TddBehaviorErrorEnvelope").annotate({
|
|
81
77
|
identifier: "TddBehaviorResult",
|
|
82
78
|
title: "tdd_behavior result",
|
|
83
79
|
description: "Discriminate on `action` (or `ok=false` for the tagged-error envelope)."
|
|
@@ -133,7 +129,7 @@ const TddBehaviorInput = Schema.Union([
|
|
|
133
129
|
* @public
|
|
134
130
|
*/
|
|
135
131
|
const handleTddBehavior = (input) => Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
|
|
136
|
-
create: (variant) =>
|
|
132
|
+
create: (variant) => Effect.gen(function* () {
|
|
137
133
|
return {
|
|
138
134
|
ok: true,
|
|
139
135
|
action: "create",
|
|
@@ -144,8 +140,13 @@ const handleTddBehavior = (input) => Match.value(input).pipe(Match.discriminator
|
|
|
144
140
|
...variant.dependsOnBehaviorIds !== void 0 && { dependsOnBehaviorIds: variant.dependsOnBehaviorIds }
|
|
145
141
|
})
|
|
146
142
|
};
|
|
143
|
+
}).pipe(ToolOutcome.catchAsEnvelope({
|
|
144
|
+
GoalNotFoundError: goalNotFound,
|
|
145
|
+
BehaviorNotFoundError: behaviorNotFound,
|
|
146
|
+
TddTaskAlreadyEndedError: tddTaskAlreadyEnded,
|
|
147
|
+
IllegalStatusTransitionError: illegalStatusTransition
|
|
147
148
|
})),
|
|
148
|
-
update: (variant) =>
|
|
149
|
+
update: (variant) => Effect.gen(function* () {
|
|
149
150
|
return {
|
|
150
151
|
ok: true,
|
|
151
152
|
action: "update",
|
|
@@ -157,15 +158,19 @@ const handleTddBehavior = (input) => Match.value(input).pipe(Match.discriminator
|
|
|
157
158
|
...variant.dependsOnBehaviorIds !== void 0 && { dependsOnBehaviorIds: variant.dependsOnBehaviorIds }
|
|
158
159
|
})
|
|
159
160
|
};
|
|
161
|
+
}).pipe(ToolOutcome.catchAsEnvelope({
|
|
162
|
+
BehaviorNotFoundError: behaviorNotFound,
|
|
163
|
+
TddTaskAlreadyEndedError: tddTaskAlreadyEnded,
|
|
164
|
+
IllegalStatusTransitionError: illegalStatusTransition
|
|
160
165
|
})),
|
|
161
|
-
delete: (variant) =>
|
|
166
|
+
delete: (variant) => Effect.gen(function* () {
|
|
162
167
|
yield* (yield* DataStore).deleteBehavior(variant.id);
|
|
163
168
|
return {
|
|
164
169
|
ok: true,
|
|
165
170
|
action: "delete",
|
|
166
171
|
id: variant.id
|
|
167
172
|
};
|
|
168
|
-
})),
|
|
173
|
+
}).pipe(ToolOutcome.catchAsEnvelope({ BehaviorNotFoundError: behaviorNotFound })),
|
|
169
174
|
get: (variant) => Effect.gen(function* () {
|
|
170
175
|
const opt = yield* (yield* DataReader).getBehaviorById(variant.id);
|
|
171
176
|
return Option.isNone(opt) ? {
|
|
@@ -178,7 +183,7 @@ const handleTddBehavior = (input) => Match.value(input).pipe(Match.discriminator
|
|
|
178
183
|
behavior: opt.value
|
|
179
184
|
};
|
|
180
185
|
}),
|
|
181
|
-
list_by_goal: (variant) =>
|
|
186
|
+
list_by_goal: (variant) => Effect.gen(function* () {
|
|
182
187
|
const store = yield* DataStore;
|
|
183
188
|
const reader = yield* DataReader;
|
|
184
189
|
yield* store.listBehaviorsByGoal(variant.goalId);
|
|
@@ -189,8 +194,8 @@ const handleTddBehavior = (input) => Match.value(input).pipe(Match.discriminator
|
|
|
189
194
|
goalId: variant.goalId,
|
|
190
195
|
behaviors
|
|
191
196
|
};
|
|
192
|
-
})),
|
|
193
|
-
list_by_tdd_task: (variant) =>
|
|
197
|
+
}).pipe(ToolOutcome.catchAsEnvelope({ GoalNotFoundError: goalNotFound })),
|
|
198
|
+
list_by_tdd_task: (variant) => Effect.gen(function* () {
|
|
194
199
|
const store = yield* DataStore;
|
|
195
200
|
const reader = yield* DataReader;
|
|
196
201
|
yield* store.listBehaviorsByTddTask(variant.tddTaskId);
|
|
@@ -201,7 +206,7 @@ const handleTddBehavior = (input) => Match.value(input).pipe(Match.discriminator
|
|
|
201
206
|
tddTaskId: variant.tddTaskId,
|
|
202
207
|
behaviors
|
|
203
208
|
};
|
|
204
|
-
}))
|
|
209
|
+
}).pipe(ToolOutcome.catchAsEnvelope({ TddTaskNotFoundError: tddTaskNotFound }))
|
|
205
210
|
})).pipe(Effect.orDie);
|
|
206
211
|
/**
|
|
207
212
|
* The Effect-native `tdd_behavior` tool.
|
package/tools/tdd-goal.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { IdempotentReplayMarker } from "../utils/replay-marker.js";
|
|
2
|
-
import {
|
|
3
|
-
import { McpToolkit,
|
|
2
|
+
import { goalNotFound, illegalStatusTransition, tddEnvelope, tddTaskAlreadyEnded, tddTaskNotFound } from "./_tdd-error-envelope.js";
|
|
3
|
+
import { McpToolkit, ToolOutcome, ToolRefusal } from "@effected/mcp";
|
|
4
4
|
import { Effect, Match, Option, Schema } from "effect";
|
|
5
|
-
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
6
5
|
import { Tool } from "effect/ai";
|
|
6
|
+
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
7
7
|
import { GoalDetail, GoalRow } from "@vitest-agent/sdk";
|
|
8
|
-
import { Remediation } from "@effected/engine";
|
|
9
8
|
|
|
10
9
|
// src/tools/tdd-goal.ts
|
|
11
10
|
/**
|
|
@@ -13,9 +12,9 @@ import { Remediation } from "@effected/engine";
|
|
|
13
12
|
*
|
|
14
13
|
* Replaces `tdd_goal_create`, `tdd_goal_update`, `tdd_goal_delete`,
|
|
15
14
|
* `tdd_goal_get`, and `tdd_goal_list` with a single tool keyed on
|
|
16
|
-
* `action`.
|
|
17
|
-
* `{ ok: false, error: { _tag,
|
|
18
|
-
*
|
|
15
|
+
* `action`. The tagged TDD errors each store call can raise are caught
|
|
16
|
+
* and surfaced as `{ ok: false, error: { _tag, message, remediation, ... } }`
|
|
17
|
+
* envelopes via `ToolOutcome.catchAsEnvelope`.
|
|
19
18
|
*/
|
|
20
19
|
const GoalStatus = Schema.Literals([
|
|
21
20
|
"pending",
|
|
@@ -23,13 +22,6 @@ const GoalStatus = Schema.Literals([
|
|
|
23
22
|
"done",
|
|
24
23
|
"abandoned"
|
|
25
24
|
]);
|
|
26
|
-
const TddErrorEnvelope = Schema.Struct({
|
|
27
|
-
ok: Schema.Literal(false).annotate({ description: "Discriminant — `false` when a tagged TDD error was caught." }),
|
|
28
|
-
error: Schema.StructWithRest(Schema.Struct({
|
|
29
|
-
_tag: Schema.String.annotate({ description: "Tagged error name (e.g. GoalNotFoundError, TddTaskNotFoundError)." }),
|
|
30
|
-
remediation: Remediation.annotate({ description: "Suggested next action: a plain-language hint, the tool to call, and its arguments." })
|
|
31
|
-
}), [Schema.Record(Schema.String, Schema.Unknown)])
|
|
32
|
-
}).annotate({ identifier: "TddErrorEnvelope" });
|
|
33
25
|
const TddGoalCreateOk = Schema.Struct({
|
|
34
26
|
ok: Schema.Literal(true),
|
|
35
27
|
action: Schema.Literal("create"),
|
|
@@ -62,15 +54,19 @@ const TddGoalListOk = Schema.Struct({
|
|
|
62
54
|
tddTaskId: Schema.Number,
|
|
63
55
|
goals: Schema.Array(GoalDetail).annotate({ description: "All goals for the TDD task, with their behaviors." })
|
|
64
56
|
}).annotate({ identifier: "TddGoalListOk" });
|
|
65
|
-
|
|
57
|
+
/**
|
|
58
|
+
* The `tdd_goal` tool's success payload.
|
|
59
|
+
*
|
|
60
|
+
* @public
|
|
61
|
+
*/
|
|
62
|
+
const TddGoalResult = tddEnvelope(Schema.Union([
|
|
66
63
|
TddGoalCreateOk,
|
|
67
64
|
TddGoalUpdateOk,
|
|
68
65
|
TddGoalDeleteOk,
|
|
69
66
|
TddGoalGetFound,
|
|
70
67
|
TddGoalGetMissing,
|
|
71
|
-
TddGoalListOk
|
|
72
|
-
|
|
73
|
-
])).annotate({
|
|
68
|
+
TddGoalListOk
|
|
69
|
+
]), "TddErrorEnvelope").annotate({
|
|
74
70
|
identifier: "TddGoalResult",
|
|
75
71
|
title: "tdd_goal result",
|
|
76
72
|
description: "Discriminate first on `action` (or on `ok=false` for the tagged-error envelope). Get returns `found:true|false`; create/update/delete/list return `ok:true` on success."
|
|
@@ -117,7 +113,7 @@ const TddGoalInput = Schema.Union([
|
|
|
117
113
|
* @public
|
|
118
114
|
*/
|
|
119
115
|
const handleTddGoal = (input) => Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
|
|
120
|
-
create: (variant) =>
|
|
116
|
+
create: (variant) => Effect.gen(function* () {
|
|
121
117
|
return {
|
|
122
118
|
ok: true,
|
|
123
119
|
action: "create",
|
|
@@ -126,8 +122,11 @@ const handleTddGoal = (input) => Match.value(input).pipe(Match.discriminatorsExh
|
|
|
126
122
|
goal: variant.goal
|
|
127
123
|
})
|
|
128
124
|
};
|
|
125
|
+
}).pipe(ToolOutcome.catchAsEnvelope({
|
|
126
|
+
TddTaskNotFoundError: tddTaskNotFound,
|
|
127
|
+
TddTaskAlreadyEndedError: tddTaskAlreadyEnded
|
|
129
128
|
})),
|
|
130
|
-
update: (variant) =>
|
|
129
|
+
update: (variant) => Effect.gen(function* () {
|
|
131
130
|
return {
|
|
132
131
|
ok: true,
|
|
133
132
|
action: "update",
|
|
@@ -137,15 +136,19 @@ const handleTddGoal = (input) => Match.value(input).pipe(Match.discriminatorsExh
|
|
|
137
136
|
...variant.status !== void 0 && { status: variant.status }
|
|
138
137
|
})
|
|
139
138
|
};
|
|
139
|
+
}).pipe(ToolOutcome.catchAsEnvelope({
|
|
140
|
+
GoalNotFoundError: goalNotFound,
|
|
141
|
+
TddTaskAlreadyEndedError: tddTaskAlreadyEnded,
|
|
142
|
+
IllegalStatusTransitionError: illegalStatusTransition
|
|
140
143
|
})),
|
|
141
|
-
delete: (variant) =>
|
|
144
|
+
delete: (variant) => Effect.gen(function* () {
|
|
142
145
|
yield* (yield* DataStore).deleteGoal(variant.id);
|
|
143
146
|
return {
|
|
144
147
|
ok: true,
|
|
145
148
|
action: "delete",
|
|
146
149
|
id: variant.id
|
|
147
150
|
};
|
|
148
|
-
})),
|
|
151
|
+
}).pipe(ToolOutcome.catchAsEnvelope({ GoalNotFoundError: goalNotFound })),
|
|
149
152
|
get: (variant) => Effect.gen(function* () {
|
|
150
153
|
const opt = yield* (yield* DataReader).getGoalById(variant.id);
|
|
151
154
|
return Option.isNone(opt) ? {
|
|
@@ -158,7 +161,7 @@ const handleTddGoal = (input) => Match.value(input).pipe(Match.discriminatorsExh
|
|
|
158
161
|
goal: opt.value
|
|
159
162
|
};
|
|
160
163
|
}),
|
|
161
|
-
list: (variant) =>
|
|
164
|
+
list: (variant) => Effect.gen(function* () {
|
|
162
165
|
const store = yield* DataStore;
|
|
163
166
|
const reader = yield* DataReader;
|
|
164
167
|
yield* store.listGoalsByTddTask(variant.tddTaskId);
|
|
@@ -169,7 +172,7 @@ const handleTddGoal = (input) => Match.value(input).pipe(Match.discriminatorsExh
|
|
|
169
172
|
tddTaskId: variant.tddTaskId,
|
|
170
173
|
goals
|
|
171
174
|
};
|
|
172
|
-
}))
|
|
175
|
+
}).pipe(ToolOutcome.catchAsEnvelope({ TddTaskNotFoundError: tddTaskNotFound }))
|
|
173
176
|
})).pipe(Effect.orDie);
|
|
174
177
|
/**
|
|
175
178
|
* The Effect-native `tdd_goal` tool.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ToolOutputSchema } from "@effected/mcp";
|
|
2
2
|
import { Effect, Option, Schema } from "effect";
|
|
3
|
-
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
4
3
|
import { Tool } from "effect/ai";
|
|
4
|
+
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
5
5
|
import { requiredArtifactForTransition, transitionEnforcesBehaviorMatch, validatePhaseTransition } from "@vitest-agent/sdk";
|
|
6
6
|
import { Remediation } from "@effected/engine";
|
|
7
7
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect, Option, Schema } from "effect";
|
|
2
|
-
import { DataReader } from "@vitest-agent/engine";
|
|
3
2
|
import { McpServer, Tool } from "effect/ai";
|
|
3
|
+
import { DataReader } from "@vitest-agent/engine";
|
|
4
4
|
import { ChannelEvent } from "@vitest-agent/sdk";
|
|
5
5
|
|
|
6
6
|
// src/tools/tdd-progress-push.ts
|
package/tools/tdd-task.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { IdempotentReplayMarker } from "../utils/replay-marker.js";
|
|
2
2
|
import { McpToolkit, ToolFailure, ToolOutputSchema, ToolRefusal } from "@effected/mcp";
|
|
3
3
|
import { Effect, Match, Option, Schema } from "effect";
|
|
4
|
-
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
5
4
|
import { Tool } from "effect/ai";
|
|
5
|
+
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
6
6
|
import { GoalDetail } from "@vitest-agent/sdk";
|
|
7
7
|
|
|
8
8
|
// src/tools/tdd-task.ts
|
|
@@ -83,6 +83,11 @@ const TddTaskResumeMissing = Schema.Struct({
|
|
|
83
83
|
found: Schema.Literal(false),
|
|
84
84
|
tddTaskId: Schema.Number
|
|
85
85
|
}).annotate({ identifier: "TddTaskResumeMissing" });
|
|
86
|
+
/**
|
|
87
|
+
* The `tdd_task` tool's success payload.
|
|
88
|
+
*
|
|
89
|
+
* @public
|
|
90
|
+
*/
|
|
86
91
|
const TddTaskResult = ToolOutputSchema.objectRooted(Schema.Union([
|
|
87
92
|
TddTaskStartOk,
|
|
88
93
|
TddTaskEndOk,
|
|
@@ -136,7 +141,7 @@ const TddTaskInput = Schema.Union([
|
|
|
136
141
|
/**
|
|
137
142
|
* Handler for {@link tddTaskTool}. A `start` with neither `sessionId` nor
|
|
138
143
|
* `chatId`, an unknown `sessionId` or `chatId`, or a blank `runId` fails with a
|
|
139
|
-
*
|
|
144
|
+
* `ToolRefusal` (an `isError` result naming the fix). A store failure is
|
|
140
145
|
* a defect.
|
|
141
146
|
*
|
|
142
147
|
* @public
|
package/tools/test.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { collectProjectRows, resolveProjectTargets } from "./_project-groups.js";
|
|
2
2
|
import { McpToolkit, ToolOutputSchema, ToolRefusal } from "@effected/mcp";
|
|
3
3
|
import { Effect, Match, Option, Schema } from "effect";
|
|
4
|
-
import { DataReader } from "@vitest-agent/engine";
|
|
5
4
|
import { Tool } from "effect/ai";
|
|
5
|
+
import { DataReader } from "@vitest-agent/engine";
|
|
6
6
|
|
|
7
7
|
// src/tools/test.ts
|
|
8
8
|
const TestRowSchema = Schema.Struct({
|
package/tools/trends.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ToolOutputSchema } from "@effected/mcp";
|
|
2
2
|
import { Effect, Option, Schema } from "effect";
|
|
3
|
-
import { DataReader } from "@vitest-agent/engine";
|
|
4
3
|
import { Tool } from "effect/ai";
|
|
4
|
+
import { DataReader } from "@vitest-agent/engine";
|
|
5
5
|
import { TrendRecord } from "@vitest-agent/sdk";
|
|
6
6
|
|
|
7
7
|
// src/tools/trends.ts
|
package/tools/triage-brief.js
CHANGED
package/tools/turn-search.js
CHANGED
package/tools/wrapup-prompt.js
CHANGED
package/version.js
CHANGED
package/idempotency.js
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { Effect, Option } from "effect";
|
|
2
|
-
import { DataReader, DataStore } from "@vitest-agent/engine";
|
|
3
|
-
|
|
4
|
-
// src/idempotency.ts
|
|
5
|
-
/**
|
|
6
|
-
* Narrow a raw tool payload to the action-shaped object every consolidated
|
|
7
|
-
* spec keys on: a non-null object carrying an `action` key. Anything else
|
|
8
|
-
* (including a legacy non-action payload) yields `null`, which every spec
|
|
9
|
-
* maps to "not idempotent".
|
|
10
|
-
*/
|
|
11
|
-
const asActionInput = (input) => input !== null && typeof input === "object" && "action" in input ? input : null;
|
|
12
|
-
/**
|
|
13
|
-
* Registered idempotency specs for mutation tools.
|
|
14
|
-
*
|
|
15
|
-
* `hypothesis validate` is covered (key: `validate:${id}:${outcome}`).
|
|
16
|
-
* `hypothesis record` is deliberately not covered — see the note in the
|
|
17
|
-
* hypothesis spec below.
|
|
18
|
-
*
|
|
19
|
-
* Add an entry here whenever a new idempotent mutation is introduced.
|
|
20
|
-
*
|
|
21
|
-
* @public
|
|
22
|
-
*/
|
|
23
|
-
const idempotencyKeys = [
|
|
24
|
-
{
|
|
25
|
-
procedurePath: "hypothesis",
|
|
26
|
-
deriveKey: (input) => {
|
|
27
|
-
const i = asActionInput(input);
|
|
28
|
-
if (i === null) return null;
|
|
29
|
-
if (i.action === "validate" && typeof i.id === "number" && typeof i.outcome === "string") return `validate:${i.id}:${i.outcome}`;
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
procedurePath: "tdd_task",
|
|
35
|
-
deriveKey: (input) => {
|
|
36
|
-
const i = asActionInput(input);
|
|
37
|
-
if (i === null) return null;
|
|
38
|
-
if (i.action === "start" && typeof i.goal === "string") {
|
|
39
|
-
if (typeof i.runId === "string") {
|
|
40
|
-
if (typeof i.sessionId === "number") return `start:sid:${i.sessionId}:run:${i.runId}`;
|
|
41
|
-
if (typeof i.chatId === "string") return `start:chat:${i.chatId}:run:${i.runId}`;
|
|
42
|
-
}
|
|
43
|
-
if (typeof i.sessionId === "number") return `start:sid:${i.sessionId}:${i.goal}`;
|
|
44
|
-
if (typeof i.chatId === "string") return `start:chat:${i.chatId}:${i.goal}`;
|
|
45
|
-
}
|
|
46
|
-
if (i.action === "end" && typeof i.tddTaskId === "number" && typeof i.outcome === "string") return `end:${i.tddTaskId}:${i.outcome}`;
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
procedurePath: "tdd_goal",
|
|
52
|
-
deriveKey: (input) => {
|
|
53
|
-
const i = asActionInput(input);
|
|
54
|
-
if (i === null) return null;
|
|
55
|
-
if (i.action === "create" && typeof i.tddTaskId === "number" && typeof i.goal === "string") return `create:${i.tddTaskId}:${i.goal}`;
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
procedurePath: "tdd_behavior",
|
|
61
|
-
deriveKey: (input) => {
|
|
62
|
-
const i = asActionInput(input);
|
|
63
|
-
if (i === null) return null;
|
|
64
|
-
if (i.action === "create" && typeof i.goalId === "number" && typeof i.behavior === "string") return `create:${i.goalId}:${i.behavior}`;
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
];
|
|
69
|
-
/** Lookup table keyed by tool name for O(1) spec retrieval. */
|
|
70
|
-
const keySpecByPath = new Map(idempotencyKeys.map((s) => [s.procedurePath, s]));
|
|
71
|
-
/** Merges the `_idempotentReplay` marker into an object payload; passes any other shape through unchanged. */
|
|
72
|
-
const withReplayMarker = (parsed) => parsed !== null && typeof parsed === "object" ? {
|
|
73
|
-
...parsed,
|
|
74
|
-
_idempotentReplay: true
|
|
75
|
-
} : parsed;
|
|
76
|
-
/**
|
|
77
|
-
* Wraps `handler` with idempotent-response caching keyed on `path`.
|
|
78
|
-
*
|
|
79
|
-
* Semantics (unchanged from the retired tRPC middleware):
|
|
80
|
-
*
|
|
81
|
-
* 1. Look up the `IdempotencyKeySpec` registered for `path`, and
|
|
82
|
-
* derive a key from `params` (already decoded — strict registration
|
|
83
|
-
* guarantees no key was stripped). No registered spec, or a `null`
|
|
84
|
-
* key, runs `handler` untouched with nothing cached.
|
|
85
|
-
* 2. Cache HIT — `DataReader.findIdempotentResponse` returns the stored
|
|
86
|
-
* JSON. A row that parses falls through to step 2a; a row that fails
|
|
87
|
-
* to parse (corrupt or truncated) is treated as step 3, a cache MISS —
|
|
88
|
-
* it is not a reason to fail the call.
|
|
89
|
-
* 2a. The parsed value is merged with `_idempotentReplay: true` for an
|
|
90
|
-
* object payload (a non-object payload passes through unchanged).
|
|
91
|
-
* `handler` does not run.
|
|
92
|
-
* 3. Cache MISS — run `handler`, then persist the result via
|
|
93
|
-
* `DataStore.recordIdempotentResponse` best-effort: a persistence
|
|
94
|
-
* failure (or a cache-lookup failure) never surfaces to the caller, it
|
|
95
|
-
* is swallowed so a transient DB failure doesn't turn into a tool
|
|
96
|
-
* error. A failed `handler` persists nothing, so a retry re-runs it.
|
|
97
|
-
*
|
|
98
|
-
* The combinator reads no ambient state — `path` and `params` are its
|
|
99
|
-
* only inputs beyond the `DataReader` / `DataStore` services it adds to
|
|
100
|
-
* `handler`'s requirements.
|
|
101
|
-
*
|
|
102
|
-
* @public
|
|
103
|
-
*/
|
|
104
|
-
const withIdempotency = (path, handler) => (params) => Effect.gen(function* () {
|
|
105
|
-
const spec = keySpecByPath.get(path);
|
|
106
|
-
const key = spec === void 0 ? null : spec.deriveKey(params);
|
|
107
|
-
if (key === null) return yield* handler(params);
|
|
108
|
-
const cached = yield* (yield* DataReader).findIdempotentResponse(path, key).pipe(Effect.orElseSucceed(() => Option.none()));
|
|
109
|
-
if (Option.isSome(cached)) {
|
|
110
|
-
const parsed = yield* Effect.try(() => JSON.parse(cached.value)).pipe(Effect.map(Option.some), Effect.orElseSucceed(() => Option.none()));
|
|
111
|
-
if (Option.isSome(parsed)) return withReplayMarker(parsed.value);
|
|
112
|
-
}
|
|
113
|
-
const result = yield* handler(params);
|
|
114
|
-
yield* (yield* DataStore).recordIdempotentResponse({
|
|
115
|
-
procedurePath: path,
|
|
116
|
-
key,
|
|
117
|
-
resultJson: JSON.stringify(result),
|
|
118
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
119
|
-
}).pipe(Effect.orElseSucceed(() => void 0));
|
|
120
|
-
return result;
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
export { idempotencyKeys, withIdempotency };
|