@vitest-agent/mcp 5.4.0 → 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.
@@ -1,6 +1,6 @@
1
1
  import { Effect, Schema } from "effect";
2
- import { DataReader } from "@vitest-agent/engine";
3
2
  import { Tool } from "effect/ai";
3
+ import { DataReader } from "@vitest-agent/engine";
4
4
 
5
5
  // src/tools/settings-list.ts
6
6
  const SettingsRow = Schema.Struct({
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
@@ -1,6 +1,6 @@
1
1
  import { Effect, Schema } from "effect";
2
- import { DataReader } from "@vitest-agent/engine";
3
2
  import { Tool } from "effect/ai";
3
+ import { DataReader } from "@vitest-agent/engine";
4
4
 
5
5
  // src/tools/tdd-artifact.ts
6
6
  const ArtifactKindSchema = Schema.Literals([
@@ -1,11 +1,10 @@
1
1
  import { IdempotentReplayMarker } from "../utils/replay-marker.js";
2
- import { catchTddErrorsAsEnvelope } from "./_tdd-error-envelope.js";
3
- import { McpToolkit, ToolOutputSchema, ToolRefusal } from "@effected/mcp";
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 = ToolOutputSchema.objectRooted(Schema.Union([
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
- TddBehaviorErrorEnvelope
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) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
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) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
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) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
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) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
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) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
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 { catchTddErrorsAsEnvelope } from "./_tdd-error-envelope.js";
3
- import { McpToolkit, ToolOutputSchema, ToolRefusal } from "@effected/mcp";
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`. All five tagged TDD errors are caught and surfaced as
17
- * `{ ok: false, error: { _tag, ..., remediation } }` envelopes via
18
- * the shared helper.
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 = ToolOutputSchema.objectRooted(Schema.Union([
62
+ const TddGoalResult = tddEnvelope(Schema.Union([
71
63
  TddGoalCreateOk,
72
64
  TddGoalUpdateOk,
73
65
  TddGoalDeleteOk,
74
66
  TddGoalGetFound,
75
67
  TddGoalGetMissing,
76
- TddGoalListOk,
77
- TddErrorEnvelope
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) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
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) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
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) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
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) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
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
@@ -1,6 +1,6 @@
1
1
  import { Effect, Schema } from "effect";
2
- import { DataReader, formatTriageEffect } from "@vitest-agent/engine";
3
2
  import { Tool } from "effect/ai";
3
+ import { DataReader, formatTriageEffect } from "@vitest-agent/engine";
4
4
 
5
5
  // src/tools/triage-brief.ts
6
6
  /**
@@ -1,6 +1,6 @@
1
1
  import { Effect, Schema } from "effect";
2
- import { DataReader } from "@vitest-agent/engine";
3
2
  import { Tool } from "effect/ai";
3
+ import { DataReader } from "@vitest-agent/engine";
4
4
 
5
5
  // src/tools/turn-search.ts
6
6
  const TurnRow = Schema.Struct({
@@ -1,6 +1,6 @@
1
1
  import { Effect, Schema } from "effect";
2
- import { DataReader, formatWrapupEffect } from "@vitest-agent/engine";
3
2
  import { Tool } from "effect/ai";
3
+ import { DataReader, formatWrapupEffect } from "@vitest-agent/engine";
4
4
 
5
5
  // src/tools/wrapup-prompt.ts
6
6
  /**
@@ -1,4 +1,4 @@
1
- import { formatFatalError } from "@vitest-agent/sdk";
1
+ import { formatFatalError } from "@vitest-agent/engine";
2
2
 
3
3
  // src/utils/safe-format-fatal-error.ts
4
4
  /**
package/version.js CHANGED
@@ -7,6 +7,6 @@
7
7
  *
8
8
  * @public
9
9
  */
10
- const CURRENT_MCP_VERSION = "5.4.0";
10
+ const CURRENT_MCP_VERSION = "6.0.0";
11
11
 
12
12
  export { CURRENT_MCP_VERSION };
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 };