@vitest-agent/mcp 5.4.0 → 6.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 +1 -1
- package/index.d.ts +707 -363
- package/index.js +1 -2
- package/main.js +8 -44
- package/package.json +11 -10
- 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 +1 -1
- 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 +23 -23
- package/tools/tdd-goal.js +22 -24
- package/tools/tdd-phase-transition-request.js +1 -1
- package/tools/tdd-progress-push.js +1 -1
- package/tools/tdd-task.js +1 -1
- 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/tsdoc-metadata.json +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"),
|
|
@@ -73,16 +65,15 @@ const TddBehaviorListByTddTaskOk = Schema.Struct({
|
|
|
73
65
|
*
|
|
74
66
|
* @public
|
|
75
67
|
*/
|
|
76
|
-
const TddBehaviorResult =
|
|
68
|
+
const TddBehaviorResult = tddEnvelope(Schema.Union([
|
|
77
69
|
TddBehaviorCreateOk,
|
|
78
70
|
TddBehaviorUpdateOk,
|
|
79
71
|
TddBehaviorDeleteOk,
|
|
80
72
|
TddBehaviorGetFound,
|
|
81
73
|
TddBehaviorGetMissing,
|
|
82
74
|
TddBehaviorListByGoalOk,
|
|
83
|
-
TddBehaviorListByTddTaskOk
|
|
84
|
-
|
|
85
|
-
])).annotate({
|
|
75
|
+
TddBehaviorListByTddTaskOk
|
|
76
|
+
]), "TddBehaviorErrorEnvelope").annotate({
|
|
86
77
|
identifier: "TddBehaviorResult",
|
|
87
78
|
title: "tdd_behavior result",
|
|
88
79
|
description: "Discriminate on `action` (or `ok=false` for the tagged-error envelope)."
|
|
@@ -138,7 +129,7 @@ const TddBehaviorInput = Schema.Union([
|
|
|
138
129
|
* @public
|
|
139
130
|
*/
|
|
140
131
|
const handleTddBehavior = (input) => Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
|
|
141
|
-
create: (variant) =>
|
|
132
|
+
create: (variant) => Effect.gen(function* () {
|
|
142
133
|
return {
|
|
143
134
|
ok: true,
|
|
144
135
|
action: "create",
|
|
@@ -149,8 +140,13 @@ const handleTddBehavior = (input) => Match.value(input).pipe(Match.discriminator
|
|
|
149
140
|
...variant.dependsOnBehaviorIds !== void 0 && { dependsOnBehaviorIds: variant.dependsOnBehaviorIds }
|
|
150
141
|
})
|
|
151
142
|
};
|
|
143
|
+
}).pipe(ToolOutcome.catchAsEnvelope({
|
|
144
|
+
GoalNotFoundError: goalNotFound,
|
|
145
|
+
BehaviorNotFoundError: behaviorNotFound,
|
|
146
|
+
TddTaskAlreadyEndedError: tddTaskAlreadyEnded,
|
|
147
|
+
IllegalStatusTransitionError: illegalStatusTransition
|
|
152
148
|
})),
|
|
153
|
-
update: (variant) =>
|
|
149
|
+
update: (variant) => Effect.gen(function* () {
|
|
154
150
|
return {
|
|
155
151
|
ok: true,
|
|
156
152
|
action: "update",
|
|
@@ -162,15 +158,19 @@ const handleTddBehavior = (input) => Match.value(input).pipe(Match.discriminator
|
|
|
162
158
|
...variant.dependsOnBehaviorIds !== void 0 && { dependsOnBehaviorIds: variant.dependsOnBehaviorIds }
|
|
163
159
|
})
|
|
164
160
|
};
|
|
161
|
+
}).pipe(ToolOutcome.catchAsEnvelope({
|
|
162
|
+
BehaviorNotFoundError: behaviorNotFound,
|
|
163
|
+
TddTaskAlreadyEndedError: tddTaskAlreadyEnded,
|
|
164
|
+
IllegalStatusTransitionError: illegalStatusTransition
|
|
165
165
|
})),
|
|
166
|
-
delete: (variant) =>
|
|
166
|
+
delete: (variant) => Effect.gen(function* () {
|
|
167
167
|
yield* (yield* DataStore).deleteBehavior(variant.id);
|
|
168
168
|
return {
|
|
169
169
|
ok: true,
|
|
170
170
|
action: "delete",
|
|
171
171
|
id: variant.id
|
|
172
172
|
};
|
|
173
|
-
})),
|
|
173
|
+
}).pipe(ToolOutcome.catchAsEnvelope({ BehaviorNotFoundError: behaviorNotFound })),
|
|
174
174
|
get: (variant) => Effect.gen(function* () {
|
|
175
175
|
const opt = yield* (yield* DataReader).getBehaviorById(variant.id);
|
|
176
176
|
return Option.isNone(opt) ? {
|
|
@@ -183,7 +183,7 @@ const handleTddBehavior = (input) => Match.value(input).pipe(Match.discriminator
|
|
|
183
183
|
behavior: opt.value
|
|
184
184
|
};
|
|
185
185
|
}),
|
|
186
|
-
list_by_goal: (variant) =>
|
|
186
|
+
list_by_goal: (variant) => Effect.gen(function* () {
|
|
187
187
|
const store = yield* DataStore;
|
|
188
188
|
const reader = yield* DataReader;
|
|
189
189
|
yield* store.listBehaviorsByGoal(variant.goalId);
|
|
@@ -194,8 +194,8 @@ const handleTddBehavior = (input) => Match.value(input).pipe(Match.discriminator
|
|
|
194
194
|
goalId: variant.goalId,
|
|
195
195
|
behaviors
|
|
196
196
|
};
|
|
197
|
-
})),
|
|
198
|
-
list_by_tdd_task: (variant) =>
|
|
197
|
+
}).pipe(ToolOutcome.catchAsEnvelope({ GoalNotFoundError: goalNotFound })),
|
|
198
|
+
list_by_tdd_task: (variant) => Effect.gen(function* () {
|
|
199
199
|
const store = yield* DataStore;
|
|
200
200
|
const reader = yield* DataReader;
|
|
201
201
|
yield* store.listBehaviorsByTddTask(variant.tddTaskId);
|
|
@@ -206,7 +206,7 @@ const handleTddBehavior = (input) => Match.value(input).pipe(Match.discriminator
|
|
|
206
206
|
tddTaskId: variant.tddTaskId,
|
|
207
207
|
behaviors
|
|
208
208
|
};
|
|
209
|
-
}))
|
|
209
|
+
}).pipe(ToolOutcome.catchAsEnvelope({ TddTaskNotFoundError: tddTaskNotFound }))
|
|
210
210
|
})).pipe(Effect.orDie);
|
|
211
211
|
/**
|
|
212
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"),
|
|
@@ -67,15 +59,14 @@ const TddGoalListOk = Schema.Struct({
|
|
|
67
59
|
*
|
|
68
60
|
* @public
|
|
69
61
|
*/
|
|
70
|
-
const TddGoalResult =
|
|
62
|
+
const TddGoalResult = tddEnvelope(Schema.Union([
|
|
71
63
|
TddGoalCreateOk,
|
|
72
64
|
TddGoalUpdateOk,
|
|
73
65
|
TddGoalDeleteOk,
|
|
74
66
|
TddGoalGetFound,
|
|
75
67
|
TddGoalGetMissing,
|
|
76
|
-
TddGoalListOk
|
|
77
|
-
|
|
78
|
-
])).annotate({
|
|
68
|
+
TddGoalListOk
|
|
69
|
+
]), "TddErrorEnvelope").annotate({
|
|
79
70
|
identifier: "TddGoalResult",
|
|
80
71
|
title: "tdd_goal result",
|
|
81
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."
|
|
@@ -122,7 +113,7 @@ const TddGoalInput = Schema.Union([
|
|
|
122
113
|
* @public
|
|
123
114
|
*/
|
|
124
115
|
const handleTddGoal = (input) => Match.value(input).pipe(Match.discriminatorsExhaustive("action")({
|
|
125
|
-
create: (variant) =>
|
|
116
|
+
create: (variant) => Effect.gen(function* () {
|
|
126
117
|
return {
|
|
127
118
|
ok: true,
|
|
128
119
|
action: "create",
|
|
@@ -131,8 +122,11 @@ const handleTddGoal = (input) => Match.value(input).pipe(Match.discriminatorsExh
|
|
|
131
122
|
goal: variant.goal
|
|
132
123
|
})
|
|
133
124
|
};
|
|
125
|
+
}).pipe(ToolOutcome.catchAsEnvelope({
|
|
126
|
+
TddTaskNotFoundError: tddTaskNotFound,
|
|
127
|
+
TddTaskAlreadyEndedError: tddTaskAlreadyEnded
|
|
134
128
|
})),
|
|
135
|
-
update: (variant) =>
|
|
129
|
+
update: (variant) => Effect.gen(function* () {
|
|
136
130
|
return {
|
|
137
131
|
ok: true,
|
|
138
132
|
action: "update",
|
|
@@ -142,15 +136,19 @@ const handleTddGoal = (input) => Match.value(input).pipe(Match.discriminatorsExh
|
|
|
142
136
|
...variant.status !== void 0 && { status: variant.status }
|
|
143
137
|
})
|
|
144
138
|
};
|
|
139
|
+
}).pipe(ToolOutcome.catchAsEnvelope({
|
|
140
|
+
GoalNotFoundError: goalNotFound,
|
|
141
|
+
TddTaskAlreadyEndedError: tddTaskAlreadyEnded,
|
|
142
|
+
IllegalStatusTransitionError: illegalStatusTransition
|
|
145
143
|
})),
|
|
146
|
-
delete: (variant) =>
|
|
144
|
+
delete: (variant) => Effect.gen(function* () {
|
|
147
145
|
yield* (yield* DataStore).deleteGoal(variant.id);
|
|
148
146
|
return {
|
|
149
147
|
ok: true,
|
|
150
148
|
action: "delete",
|
|
151
149
|
id: variant.id
|
|
152
150
|
};
|
|
153
|
-
})),
|
|
151
|
+
}).pipe(ToolOutcome.catchAsEnvelope({ GoalNotFoundError: goalNotFound })),
|
|
154
152
|
get: (variant) => Effect.gen(function* () {
|
|
155
153
|
const opt = yield* (yield* DataReader).getGoalById(variant.id);
|
|
156
154
|
return Option.isNone(opt) ? {
|
|
@@ -163,7 +161,7 @@ const handleTddGoal = (input) => Match.value(input).pipe(Match.discriminatorsExh
|
|
|
163
161
|
goal: opt.value
|
|
164
162
|
};
|
|
165
163
|
}),
|
|
166
|
-
list: (variant) =>
|
|
164
|
+
list: (variant) => Effect.gen(function* () {
|
|
167
165
|
const store = yield* DataStore;
|
|
168
166
|
const reader = yield* DataReader;
|
|
169
167
|
yield* store.listGoalsByTddTask(variant.tddTaskId);
|
|
@@ -174,7 +172,7 @@ const handleTddGoal = (input) => Match.value(input).pipe(Match.discriminatorsExh
|
|
|
174
172
|
tddTaskId: variant.tddTaskId,
|
|
175
173
|
goals
|
|
176
174
|
};
|
|
177
|
-
}))
|
|
175
|
+
}).pipe(ToolOutcome.catchAsEnvelope({ TddTaskNotFoundError: tddTaskNotFound }))
|
|
178
176
|
})).pipe(Effect.orDie);
|
|
179
177
|
/**
|
|
180
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
|
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/tsdoc-metadata.json
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 };
|