@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.
@@ -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"),
@@ -68,16 +60,20 @@ const TddBehaviorListByTddTaskOk = Schema.Struct({
68
60
  tddTaskId: Schema.Number,
69
61
  behaviors: Schema.Array(BehaviorRow)
70
62
  });
71
- const TddBehaviorResult = ToolOutputSchema.objectRooted(Schema.Union([
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
- TddBehaviorErrorEnvelope
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) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
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) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
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) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
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) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
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) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
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 { 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"),
@@ -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
- const TddGoalResult = ToolOutputSchema.objectRooted(Schema.Union([
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
- TddErrorEnvelope
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) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
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) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
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) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
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) => catchTddErrorsAsEnvelope(Effect.gen(function* () {
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
- * {@link ToolRefusal} (an `isError` result naming the fix). A store failure is
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
@@ -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.3.1";
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 };