@sellable/mcp 0.1.793 → 0.1.794
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/dist/agent-tool-names.d.ts +2 -2
- package/dist/agent-tool-names.js +14 -0
- package/dist/scheduler-envelope.d.ts +190 -0
- package/dist/scheduler-envelope.js +412 -0
- package/dist/server.js +28 -2
- package/dist/tools/campaign-message-preparation.d.ts +25 -0
- package/dist/tools/campaign-message-preparation.js +37 -0
- package/dist/tools/campaign-processing.d.ts +74 -1
- package/dist/tools/campaign-processing.js +57 -1
- package/dist/tools/prompts.d.ts +15 -3
- package/dist/tools/prompts.js +25 -0
- package/dist/tools/refill-executors.d.ts +43 -70
- package/dist/tools/refill-executors.js +201 -273
- package/dist/tools/refill-sends.d.ts +3 -3
- package/dist/tools/refill-sends.js +1 -1
- package/dist/tools/refill-target-plan.js +1 -1
- package/dist/tools/refill-v3-advance-contract.d.ts +618 -0
- package/dist/tools/refill-v3-advance-contract.js +920 -0
- package/dist/tools/refill-v3-advance.d.ts +397 -0
- package/dist/tools/refill-v3-advance.js +75 -0
- package/dist/tools/refill-v3-attention-packet-contract.d.ts +608 -0
- package/dist/tools/refill-v3-attention-packet-contract.js +911 -0
- package/dist/tools/refill-v3-campaign-work.d.ts +324 -0
- package/dist/tools/refill-v3-campaign-work.js +1015 -0
- package/dist/tools/refill-v3-continue-contract.d.ts +433 -0
- package/dist/tools/refill-v3-continue-contract.js +112 -0
- package/dist/tools/refill-v3-continue.d.ts +501 -0
- package/dist/tools/refill-v3-continue.js +654 -0
- package/dist/tools/refill-v3-edit-selected-campaign-contract.d.ts +208 -0
- package/dist/tools/refill-v3-edit-selected-campaign-contract.js +351 -0
- package/dist/tools/refill-v3-edit-selected-campaign.d.ts +232 -0
- package/dist/tools/refill-v3-edit-selected-campaign.js +147 -0
- package/dist/tools/refill-v3-fill-ready-contract.d.ts +198 -0
- package/dist/tools/refill-v3-fill-ready-contract.js +324 -0
- package/dist/tools/refill-v3-fill-ready.d.ts +199 -0
- package/dist/tools/refill-v3-fill-ready.js +136 -0
- package/dist/tools/refill-v3-source-family-dispatch.d.ts +249 -0
- package/dist/tools/refill-v3-source-family-dispatch.js +514 -0
- package/dist/tools/refill-v3-waterfall-contract.d.ts +156 -0
- package/dist/tools/refill-v3-waterfall-contract.js +181 -0
- package/dist/tools/refill-v3-waterfall.d.ts +136 -0
- package/dist/tools/refill-v3-waterfall.js +161 -0
- package/dist/tools/refill-v3-world-state-contract.d.ts +666 -0
- package/dist/tools/refill-v3-world-state-contract.js +790 -0
- package/dist/tools/refill-v3-world-state.d.ts +592 -0
- package/dist/tools/refill-v3-world-state.js +137 -0
- package/dist/tools/registry.d.ts +1710 -3
- package/dist/tools/registry.js +19 -0
- package/package.json +1 -1
- package/skills/refill-sends/SKILL.md +89 -4
- package/skills/refill-sends-waterfall-order/SKILL.md +174 -0
- package/skills/refill-sends-work-campaign/SKILL.md +253 -0
- package/skills/refill-sends-workflow/SKILL.md +112 -5
- package/skills/refill-sends-workflow/core/flow.v3.json +209 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refill V3 fill-ready PACKAGE seam.
|
|
3
|
+
*
|
|
4
|
+
* This module is the SOLE owner of the fill-ready public result contract:
|
|
5
|
+
* - `REFILL_V3_FILL_READY_OUTCOMES` and the closed reason/receipt/result types;
|
|
6
|
+
* - `REFILL_V3_FILL_READY_REASON_CODES`, which EXTENDS the world-state code
|
|
7
|
+
* vocabulary rather than forking it;
|
|
8
|
+
* - `decodeRefillV3FillReadyResult`, the ONE unknown-value decoder; and
|
|
9
|
+
* - `REFILL_V3_FILL_READY_OUTPUT_SCHEMA`, derived from the same closed
|
|
10
|
+
* vocabulary so the central MCP envelope can always emit matching
|
|
11
|
+
* `structuredContent`.
|
|
12
|
+
*
|
|
13
|
+
* It lives inside `@sellable/mcp` and compiles to
|
|
14
|
+
* `dist/tools/refill-v3-fill-ready-contract.js` plus `.d.ts`, so both the app
|
|
15
|
+
* service and the Plan 08 tool wrapper IMPORT these exact exports. Nothing here
|
|
16
|
+
* imports app `src/**`, and no consumer may copy the types, outcomes, decoder,
|
|
17
|
+
* or schema.
|
|
18
|
+
*
|
|
19
|
+
* Decoding rules: hand-rolled type guards plus as-const JSON-Schema literals,
|
|
20
|
+
* strict inbound, and fail-closed. Unlike the world-state observation, a
|
|
21
|
+
* fill-ready result describes a MUTATION, so an unrecognized member is rejected
|
|
22
|
+
* outright instead of being narrowed to a tolerant projection: a malformed
|
|
23
|
+
* mutation receipt must never be read as a partial success.
|
|
24
|
+
*/
|
|
25
|
+
import { REFILL_V3_WORLD_STATE_FORBIDDEN_LEAK_KEYS, REFILL_V3_WORLD_STATE_LANES, REFILL_V3_WORLD_STATE_REASON_CODES, } from "./refill-v3-world-state-contract.js";
|
|
26
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
27
|
+
// Closed output vocabularies
|
|
28
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
29
|
+
export const REFILL_V3_FILL_READY_SCHEMA_NAMESPACE = "refill_v3_fill_ready";
|
|
30
|
+
export const REFILL_V3_FILL_READY_SCHEMA_MAJOR = 1;
|
|
31
|
+
export const REFILL_V3_FILL_READY_SCHEMA_VERSION = "refill_v3_fill_ready.v1.0";
|
|
32
|
+
/** The five terminal outcomes. There is deliberately no sixth member. */
|
|
33
|
+
export const REFILL_V3_FILL_READY_OUTCOMES = [
|
|
34
|
+
"scheduled",
|
|
35
|
+
"partially_scheduled",
|
|
36
|
+
"awaiting_scheduler",
|
|
37
|
+
"campaign_exhausted",
|
|
38
|
+
"blocked",
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* Codes emitted ONLY by the fill-ready service. They EXTEND the world-state
|
|
42
|
+
* vocabulary; every one gets a row in the single exported code -> classification
|
|
43
|
+
* map, so retryability stays a property of the CODE rather than of a call site.
|
|
44
|
+
*/
|
|
45
|
+
export const REFILL_V3_FILL_READY_ONLY_REASON_CODES = [
|
|
46
|
+
/** No usable positive requested count could be derived. */
|
|
47
|
+
"zero_requested_count",
|
|
48
|
+
/** The caller key was reused with a changed exact scope. */
|
|
49
|
+
"idempotency_scope_mismatch",
|
|
50
|
+
/** Placement outcome unknown; never blindly retried under the same key. */
|
|
51
|
+
"uncertain_outcome",
|
|
52
|
+
/** A concurrent writer consumed the cohort under another sender. */
|
|
53
|
+
"placed_under_other_sender",
|
|
54
|
+
/** A concurrent writer consumed the cohort on another target date. */
|
|
55
|
+
"placed_off_target_date",
|
|
56
|
+
/** The paid-credit receipt is invalid at the schedule gate. */
|
|
57
|
+
"paid_inmail_credit_receipt_invalid",
|
|
58
|
+
/** A bounded poll budget expired with the stored receipt unchanged. */
|
|
59
|
+
"awaiting_external_change",
|
|
60
|
+
"approval_authority_invalid",
|
|
61
|
+
/**
|
|
62
|
+
* Another message-preparation job currently owns this campaign's approvals.
|
|
63
|
+
* That is a WAIT, not a configuration defect: the same request succeeds once
|
|
64
|
+
* the other preparation finishes (Plan 12, ARD-FINDING-5).
|
|
65
|
+
*/
|
|
66
|
+
"approval_authority_in_flight",
|
|
67
|
+
"approval_dispatch_incomplete",
|
|
68
|
+
"ready_cells_unavailable",
|
|
69
|
+
"sender_affinity_conflict",
|
|
70
|
+
"scheduler_target_omitted",
|
|
71
|
+
"scheduler_failed",
|
|
72
|
+
"receipt_readback_mismatch",
|
|
73
|
+
];
|
|
74
|
+
/** The complete closed fill-ready code vocabulary. */
|
|
75
|
+
export const REFILL_V3_FILL_READY_REASON_CODES = [
|
|
76
|
+
...REFILL_V3_WORLD_STATE_REASON_CODES,
|
|
77
|
+
...REFILL_V3_FILL_READY_ONLY_REASON_CODES,
|
|
78
|
+
];
|
|
79
|
+
/**
|
|
80
|
+
* Why a request was reduced. A clamp is NEVER silent: whenever
|
|
81
|
+
* `requestedCount !== appliedCount` the result carries this basis, except for
|
|
82
|
+
* the two concurrent-consumption codes where the shortfall is not a clamp at
|
|
83
|
+
* all and labelling it one would be a false statement.
|
|
84
|
+
*/
|
|
85
|
+
export const REFILL_V3_FILL_READY_CLAMP_BASES = [
|
|
86
|
+
"capacity",
|
|
87
|
+
"supply",
|
|
88
|
+
"candidates",
|
|
89
|
+
];
|
|
90
|
+
/** Codes for which a shortfall is concurrent consumption, never a clamp. */
|
|
91
|
+
export const REFILL_V3_FILL_READY_CLAMP_EXEMPT_CODES = [
|
|
92
|
+
"placed_under_other_sender",
|
|
93
|
+
"placed_off_target_date",
|
|
94
|
+
];
|
|
95
|
+
/** Reused verbatim: a mutation result must not leak identity or internals. */
|
|
96
|
+
export const REFILL_V3_FILL_READY_FORBIDDEN_LEAK_KEYS = REFILL_V3_WORLD_STATE_FORBIDDEN_LEAK_KEYS;
|
|
97
|
+
export const REFILL_V3_FILL_READY_RESULT_KEYS = [
|
|
98
|
+
"outcome",
|
|
99
|
+
"requestedCount",
|
|
100
|
+
"appliedCount",
|
|
101
|
+
"approvedCount",
|
|
102
|
+
"scheduledCount",
|
|
103
|
+
"reason",
|
|
104
|
+
"clamp",
|
|
105
|
+
"receipt",
|
|
106
|
+
"pollKey",
|
|
107
|
+
];
|
|
108
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
109
|
+
// Public output schema
|
|
110
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
111
|
+
const COUNT_SCHEMA = { type: "integer", minimum: 0 };
|
|
112
|
+
const FILL_READY_REASON_SCHEMA = {
|
|
113
|
+
type: ["object", "null"],
|
|
114
|
+
additionalProperties: false,
|
|
115
|
+
required: ["code", "message", "retryable"],
|
|
116
|
+
properties: {
|
|
117
|
+
code: { type: "string", enum: [...REFILL_V3_FILL_READY_REASON_CODES] },
|
|
118
|
+
message: { type: "string" },
|
|
119
|
+
retryable: { type: "boolean" },
|
|
120
|
+
resumeAt: { type: "string" },
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
const FILL_READY_CLAMP_SCHEMA = {
|
|
124
|
+
type: ["object", "null"],
|
|
125
|
+
additionalProperties: false,
|
|
126
|
+
required: ["basis", "requested", "limit"],
|
|
127
|
+
properties: {
|
|
128
|
+
basis: { type: "string", enum: [...REFILL_V3_FILL_READY_CLAMP_BASES] },
|
|
129
|
+
requested: COUNT_SCHEMA,
|
|
130
|
+
limit: COUNT_SCHEMA,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
const FILL_READY_RECEIPT_SCHEMA = {
|
|
134
|
+
type: "object",
|
|
135
|
+
additionalProperties: false,
|
|
136
|
+
required: ["idempotencyKey", "campaignId", "senderId", "targetDate"],
|
|
137
|
+
properties: {
|
|
138
|
+
idempotencyKey: { type: "string", minLength: 1 },
|
|
139
|
+
campaignId: { type: "string", minLength: 1 },
|
|
140
|
+
senderId: { type: "string", minLength: 1 },
|
|
141
|
+
targetDate: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" },
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* A TOP-LEVEL OBJECT, never an array: an MCP client validates
|
|
146
|
+
* `structuredContent` against this declaration, and a tool that declares an
|
|
147
|
+
* output schema and answers with a shape the client rejects has its ENTIRE
|
|
148
|
+
* response discarded. Every boundary the decoder closes is closed here too.
|
|
149
|
+
*/
|
|
150
|
+
export const REFILL_V3_FILL_READY_OUTPUT_SCHEMA = Object.freeze({
|
|
151
|
+
type: "object",
|
|
152
|
+
additionalProperties: false,
|
|
153
|
+
required: [...REFILL_V3_FILL_READY_RESULT_KEYS],
|
|
154
|
+
properties: {
|
|
155
|
+
outcome: { type: "string", enum: [...REFILL_V3_FILL_READY_OUTCOMES] },
|
|
156
|
+
requestedCount: COUNT_SCHEMA,
|
|
157
|
+
appliedCount: COUNT_SCHEMA,
|
|
158
|
+
approvedCount: COUNT_SCHEMA,
|
|
159
|
+
scheduledCount: COUNT_SCHEMA,
|
|
160
|
+
reason: FILL_READY_REASON_SCHEMA,
|
|
161
|
+
clamp: FILL_READY_CLAMP_SCHEMA,
|
|
162
|
+
receipt: FILL_READY_RECEIPT_SCHEMA,
|
|
163
|
+
pollKey: { type: ["string", "null"] },
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
167
|
+
// Sole unknown-value decoder
|
|
168
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
169
|
+
function isRecord(value) {
|
|
170
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
171
|
+
}
|
|
172
|
+
export function isOneOfFillReady(value, allowed) {
|
|
173
|
+
return (typeof value === "string" && allowed.includes(value));
|
|
174
|
+
}
|
|
175
|
+
/** Transport-only key the encoder adds. It is stripped, never business. */
|
|
176
|
+
const FILL_READY_TRANSPORT_KEYS = ["schemaVersion"];
|
|
177
|
+
const CANONICAL_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
178
|
+
function onlyKeys(record, allowed) {
|
|
179
|
+
return Object.keys(record).every((key) => allowed.includes(key));
|
|
180
|
+
}
|
|
181
|
+
function nonNegativeCount(value) {
|
|
182
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
183
|
+
}
|
|
184
|
+
function nonEmptyString(value) {
|
|
185
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
186
|
+
}
|
|
187
|
+
function decodeFillReadyReason(value) {
|
|
188
|
+
if (value === null)
|
|
189
|
+
return null;
|
|
190
|
+
if (!isRecord(value))
|
|
191
|
+
return "invalid";
|
|
192
|
+
if (!onlyKeys(value, ["code", "message", "retryable", "resumeAt"]) ||
|
|
193
|
+
!isOneOfFillReady(value.code, REFILL_V3_FILL_READY_REASON_CODES) ||
|
|
194
|
+
typeof value.message !== "string" ||
|
|
195
|
+
typeof value.retryable !== "boolean") {
|
|
196
|
+
return "invalid";
|
|
197
|
+
}
|
|
198
|
+
if (value.resumeAt !== undefined && !nonEmptyString(value.resumeAt)) {
|
|
199
|
+
return "invalid";
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
code: value.code,
|
|
203
|
+
message: value.message,
|
|
204
|
+
retryable: value.retryable,
|
|
205
|
+
...(value.resumeAt === undefined
|
|
206
|
+
? {}
|
|
207
|
+
: { resumeAt: value.resumeAt }),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function decodeFillReadyClamp(value) {
|
|
211
|
+
if (value === null)
|
|
212
|
+
return null;
|
|
213
|
+
if (!isRecord(value))
|
|
214
|
+
return "invalid";
|
|
215
|
+
if (!onlyKeys(value, ["basis", "requested", "limit"]) ||
|
|
216
|
+
!isOneOfFillReady(value.basis, REFILL_V3_FILL_READY_CLAMP_BASES) ||
|
|
217
|
+
!nonNegativeCount(value.requested) ||
|
|
218
|
+
!nonNegativeCount(value.limit)) {
|
|
219
|
+
return "invalid";
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
basis: value.basis,
|
|
223
|
+
requested: value.requested,
|
|
224
|
+
limit: value.limit,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function decodeFillReadyReceipt(value) {
|
|
228
|
+
if (!isRecord(value))
|
|
229
|
+
return null;
|
|
230
|
+
const keys = ["idempotencyKey", "campaignId", "senderId", "targetDate"];
|
|
231
|
+
if (!onlyKeys(value, keys))
|
|
232
|
+
return null;
|
|
233
|
+
if (keys.some((key) => !nonEmptyString(value[key])))
|
|
234
|
+
return null;
|
|
235
|
+
if (!CANONICAL_DATE.test(value.targetDate))
|
|
236
|
+
return null;
|
|
237
|
+
return {
|
|
238
|
+
idempotencyKey: value.idempotencyKey,
|
|
239
|
+
campaignId: value.campaignId,
|
|
240
|
+
senderId: value.senderId,
|
|
241
|
+
targetDate: value.targetDate,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Sole unknown-value decoder for the public result. STRICT and FAIL-CLOSED: a
|
|
246
|
+
* fill-ready result describes a MUTATION, so an unknown key, a malformed count,
|
|
247
|
+
* a broken count invariant, or a leaked identity rejects the whole value rather
|
|
248
|
+
* than being narrowed into a confident partial success.
|
|
249
|
+
*/
|
|
250
|
+
export function decodeRefillV3FillReadyResult(value) {
|
|
251
|
+
if (!isRecord(value))
|
|
252
|
+
return null;
|
|
253
|
+
if (!onlyKeys(value, [
|
|
254
|
+
...REFILL_V3_FILL_READY_RESULT_KEYS,
|
|
255
|
+
...FILL_READY_TRANSPORT_KEYS,
|
|
256
|
+
])) {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
if (REFILL_V3_FILL_READY_RESULT_KEYS.some((key) => !(key in value))) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
if (!isOneOfFillReady(value.outcome, REFILL_V3_FILL_READY_OUTCOMES)) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
if (!nonNegativeCount(value.requestedCount) ||
|
|
266
|
+
!nonNegativeCount(value.appliedCount) ||
|
|
267
|
+
!nonNegativeCount(value.approvedCount) ||
|
|
268
|
+
!nonNegativeCount(value.scheduledCount)) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
if (value.appliedCount > value.requestedCount ||
|
|
272
|
+
value.approvedCount > value.requestedCount ||
|
|
273
|
+
value.scheduledCount > value.requestedCount) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
const reason = decodeFillReadyReason(value.reason);
|
|
277
|
+
if (reason === "invalid")
|
|
278
|
+
return null;
|
|
279
|
+
const clamp = decodeFillReadyClamp(value.clamp);
|
|
280
|
+
if (clamp === "invalid")
|
|
281
|
+
return null;
|
|
282
|
+
const receipt = decodeFillReadyReceipt(value.receipt);
|
|
283
|
+
if (!receipt)
|
|
284
|
+
return null;
|
|
285
|
+
if (value.pollKey !== null && !nonEmptyString(value.pollKey))
|
|
286
|
+
return null;
|
|
287
|
+
return {
|
|
288
|
+
outcome: value.outcome,
|
|
289
|
+
requestedCount: value.requestedCount,
|
|
290
|
+
appliedCount: value.appliedCount,
|
|
291
|
+
approvedCount: value.approvedCount,
|
|
292
|
+
scheduledCount: value.scheduledCount,
|
|
293
|
+
reason,
|
|
294
|
+
clamp,
|
|
295
|
+
receipt,
|
|
296
|
+
pollKey: value.pollKey,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
/** Wire form of a decoded result. `schemaVersion` is TRANSPORT, not business. */
|
|
300
|
+
export function encodeRefillV3FillReadyResult(result) {
|
|
301
|
+
return {
|
|
302
|
+
schemaVersion: REFILL_V3_FILL_READY_SCHEMA_VERSION,
|
|
303
|
+
outcome: result.outcome,
|
|
304
|
+
requestedCount: result.requestedCount,
|
|
305
|
+
appliedCount: result.appliedCount,
|
|
306
|
+
approvedCount: result.approvedCount,
|
|
307
|
+
scheduledCount: result.scheduledCount,
|
|
308
|
+
reason: result.reason
|
|
309
|
+
? {
|
|
310
|
+
code: result.reason.code,
|
|
311
|
+
message: result.reason.message,
|
|
312
|
+
retryable: result.reason.retryable,
|
|
313
|
+
...(result.reason.resumeAt === undefined
|
|
314
|
+
? {}
|
|
315
|
+
: { resumeAt: result.reason.resumeAt }),
|
|
316
|
+
}
|
|
317
|
+
: null,
|
|
318
|
+
clamp: result.clamp ? { ...result.clamp } : null,
|
|
319
|
+
receipt: { ...result.receipt },
|
|
320
|
+
pollKey: result.pollKey,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/** The two lanes, re-exported so a consumer needs exactly one import. */
|
|
324
|
+
export const REFILL_V3_FILL_READY_LANES = REFILL_V3_WORLD_STATE_LANES;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refill V3 fill-ready PUBLIC MCP projection (Phase 146.5 Plan 08, RV3MCP-002).
|
|
3
|
+
*
|
|
4
|
+
* This is a THIN projection over Plan 07's package-root contract and nothing
|
|
5
|
+
* else:
|
|
6
|
+
*
|
|
7
|
+
* 1. it mirrors Plan 07's closed seven-key request vocabulary so a
|
|
8
|
+
* protocol-level validator refuses drift — including the alias `count` —
|
|
9
|
+
* before the handler runs;
|
|
10
|
+
* 2. it applies the ONE shared explicit-workspace guard, because a bounded
|
|
11
|
+
* mutation must name its target workspace instead of switching the shared
|
|
12
|
+
* active workspace;
|
|
13
|
+
* 3. it posts the caller's exact values once to the authenticated route, whose
|
|
14
|
+
* `parseRefillV3FillReadyInput` is the SOLE runtime authority; and
|
|
15
|
+
* 4. it returns only what Plan 07's SOLE closed decoder produced.
|
|
16
|
+
*
|
|
17
|
+
* It holds no approval, scheduler, cohort, clamp, or idempotency policy: the
|
|
18
|
+
* caller's replay identity is forwarded byte-for-byte, because rewriting it
|
|
19
|
+
* would silently turn a replay into a second mutation scope.
|
|
20
|
+
*
|
|
21
|
+
* A typed business blocker — including `blocked`, `campaign_exhausted`, and
|
|
22
|
+
* `awaiting_scheduler` — is a successful response VALUE and passes through
|
|
23
|
+
* VERBATIM through the central `toMcpToolResult`. Transport failure is the ONLY
|
|
24
|
+
* source of an error result (error law 6), so this module contains no branch
|
|
25
|
+
* that reads a decoded business field to decide failure.
|
|
26
|
+
*
|
|
27
|
+
* The declarative input schema is mirrored here rather than imported because the
|
|
28
|
+
* published package must never import app `src/**`. The mirror is not trusted:
|
|
29
|
+
* `tests/mcp/refill-v3/fill-ready-tool.test.ts` asserts its required key set and
|
|
30
|
+
* property set equal Plan 07's `REFILL_V3_FILL_READY_INPUT_KEYS`, so drift is a
|
|
31
|
+
* failing pin.
|
|
32
|
+
*/
|
|
33
|
+
import { type RefillV3FillReadyResult } from "./refill-v3-fill-ready-contract.js";
|
|
34
|
+
/** The one authenticated backend boundary this projection may reach. */
|
|
35
|
+
export declare const REFILL_V3_FILL_READY_ROUTE = "/api/v3/mcp/refill-v3/fill-ready";
|
|
36
|
+
export declare const REFILL_V3_FILL_READY_TOOL_NAME: "refill_v3_fill_ready";
|
|
37
|
+
export declare const refillV3FillReadyToolDefinitions: {
|
|
38
|
+
name: "refill_v3_fill_ready";
|
|
39
|
+
description: string;
|
|
40
|
+
inputSchema: {
|
|
41
|
+
readonly type: "object";
|
|
42
|
+
readonly additionalProperties: false;
|
|
43
|
+
readonly required: readonly ["workspaceId", "senderId", "targetDate", "lane", "campaignId", "requestedCount", "idempotencyKey"];
|
|
44
|
+
readonly properties: {
|
|
45
|
+
readonly workspaceId: {
|
|
46
|
+
readonly type: "string";
|
|
47
|
+
readonly minLength: 1;
|
|
48
|
+
readonly description: "Explicit request-scoped workspace id. Pass this instead of switching the shared active workspace.";
|
|
49
|
+
};
|
|
50
|
+
readonly senderId: {
|
|
51
|
+
readonly type: "string";
|
|
52
|
+
readonly minLength: 1;
|
|
53
|
+
readonly description: "Exact Sender.id. Never pass a sender name.";
|
|
54
|
+
};
|
|
55
|
+
readonly targetDate: {
|
|
56
|
+
readonly type: "string";
|
|
57
|
+
readonly pattern: "^\\d{4}-\\d{2}-\\d{2}$";
|
|
58
|
+
readonly description: "The sender-local calendar date to fill, YYYY-MM-DD.";
|
|
59
|
+
};
|
|
60
|
+
readonly lane: {
|
|
61
|
+
readonly type: "string";
|
|
62
|
+
readonly enum: readonly ["connection_invite", "paid_inmail"];
|
|
63
|
+
readonly description: "The exact lane to fill. The two lanes are independent.";
|
|
64
|
+
};
|
|
65
|
+
readonly campaignId: {
|
|
66
|
+
readonly type: "string";
|
|
67
|
+
readonly minLength: 1;
|
|
68
|
+
readonly description: "Exact id of the ONE campaign this request may touch, chosen from the world-state options.";
|
|
69
|
+
};
|
|
70
|
+
readonly requestedCount: {
|
|
71
|
+
readonly type: "integer";
|
|
72
|
+
readonly minimum: 0;
|
|
73
|
+
readonly description: "How many actions to place. The backend clamps it to real capacity, supply, and clean candidates and names the binding limit; 0 is a valid bounded no-op.";
|
|
74
|
+
};
|
|
75
|
+
readonly idempotencyKey: {
|
|
76
|
+
readonly type: "string";
|
|
77
|
+
readonly minLength: 1;
|
|
78
|
+
readonly description: "Caller replay identity. Reuse the SAME key to retry one request; a new key means a new placement scope.";
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
outputSchema: Readonly<{
|
|
83
|
+
readonly type: "object";
|
|
84
|
+
readonly additionalProperties: false;
|
|
85
|
+
readonly required: readonly ["outcome", "requestedCount", "appliedCount", "approvedCount", "scheduledCount", "reason", "clamp", "receipt", "pollKey"];
|
|
86
|
+
readonly properties: {
|
|
87
|
+
readonly outcome: {
|
|
88
|
+
readonly type: "string";
|
|
89
|
+
readonly enum: readonly ["scheduled", "partially_scheduled", "awaiting_scheduler", "campaign_exhausted", "blocked"];
|
|
90
|
+
};
|
|
91
|
+
readonly requestedCount: {
|
|
92
|
+
readonly type: "integer";
|
|
93
|
+
readonly minimum: 0;
|
|
94
|
+
};
|
|
95
|
+
readonly appliedCount: {
|
|
96
|
+
readonly type: "integer";
|
|
97
|
+
readonly minimum: 0;
|
|
98
|
+
};
|
|
99
|
+
readonly approvedCount: {
|
|
100
|
+
readonly type: "integer";
|
|
101
|
+
readonly minimum: 0;
|
|
102
|
+
};
|
|
103
|
+
readonly scheduledCount: {
|
|
104
|
+
readonly type: "integer";
|
|
105
|
+
readonly minimum: 0;
|
|
106
|
+
};
|
|
107
|
+
readonly reason: {
|
|
108
|
+
readonly type: readonly ["object", "null"];
|
|
109
|
+
readonly additionalProperties: false;
|
|
110
|
+
readonly required: readonly ["code", "message", "retryable"];
|
|
111
|
+
readonly properties: {
|
|
112
|
+
readonly code: {
|
|
113
|
+
readonly type: "string";
|
|
114
|
+
readonly enum: readonly ["global_cooldown", "connection_cooldown", "paid_inmail_cooldown", "endpoint_temporarily_unavailable", "date_in_past", "no_sending_hours", "sender_disconnected", "sender_limit_reached", "daily_target_met", "no_eligible_campaign", "no_ready_supply", "sales_nav_disconnected", "insufficient_paid_credits", "paid_inmail_connection_health_recheck_required", "paid_inmail_credit_refresh_failed", "invalid_paid_configuration", "campaign_table_missing", "campaign_sequence_invalid", "source_family_unresolved", "campaign_work_in_flight", "zero_requested_count", "idempotency_scope_mismatch", "uncertain_outcome", "placed_under_other_sender", "placed_off_target_date", "paid_inmail_credit_receipt_invalid", "awaiting_external_change", "approval_authority_invalid", "approval_authority_in_flight", "approval_dispatch_incomplete", "ready_cells_unavailable", "sender_affinity_conflict", "scheduler_target_omitted", "scheduler_failed", "receipt_readback_mismatch"];
|
|
115
|
+
};
|
|
116
|
+
readonly message: {
|
|
117
|
+
readonly type: "string";
|
|
118
|
+
};
|
|
119
|
+
readonly retryable: {
|
|
120
|
+
readonly type: "boolean";
|
|
121
|
+
};
|
|
122
|
+
readonly resumeAt: {
|
|
123
|
+
readonly type: "string";
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
readonly clamp: {
|
|
128
|
+
readonly type: readonly ["object", "null"];
|
|
129
|
+
readonly additionalProperties: false;
|
|
130
|
+
readonly required: readonly ["basis", "requested", "limit"];
|
|
131
|
+
readonly properties: {
|
|
132
|
+
readonly basis: {
|
|
133
|
+
readonly type: "string";
|
|
134
|
+
readonly enum: readonly ["capacity", "supply", "candidates"];
|
|
135
|
+
};
|
|
136
|
+
readonly requested: {
|
|
137
|
+
readonly type: "integer";
|
|
138
|
+
readonly minimum: 0;
|
|
139
|
+
};
|
|
140
|
+
readonly limit: {
|
|
141
|
+
readonly type: "integer";
|
|
142
|
+
readonly minimum: 0;
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
};
|
|
146
|
+
readonly receipt: {
|
|
147
|
+
readonly type: "object";
|
|
148
|
+
readonly additionalProperties: false;
|
|
149
|
+
readonly required: readonly ["idempotencyKey", "campaignId", "senderId", "targetDate"];
|
|
150
|
+
readonly properties: {
|
|
151
|
+
readonly idempotencyKey: {
|
|
152
|
+
readonly type: "string";
|
|
153
|
+
readonly minLength: 1;
|
|
154
|
+
};
|
|
155
|
+
readonly campaignId: {
|
|
156
|
+
readonly type: "string";
|
|
157
|
+
readonly minLength: 1;
|
|
158
|
+
};
|
|
159
|
+
readonly senderId: {
|
|
160
|
+
readonly type: "string";
|
|
161
|
+
readonly minLength: 1;
|
|
162
|
+
};
|
|
163
|
+
readonly targetDate: {
|
|
164
|
+
readonly type: "string";
|
|
165
|
+
readonly pattern: "^\\d{4}-\\d{2}-\\d{2}$";
|
|
166
|
+
};
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
readonly pollKey: {
|
|
170
|
+
readonly type: readonly ["string", "null"];
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
}>;
|
|
174
|
+
annotations: {
|
|
175
|
+
title: string;
|
|
176
|
+
readOnlyHint: boolean;
|
|
177
|
+
destructiveHint: boolean;
|
|
178
|
+
idempotentHint: boolean;
|
|
179
|
+
openWorldHint: boolean;
|
|
180
|
+
};
|
|
181
|
+
}[];
|
|
182
|
+
export type RefillV3FillReadyToolInput = {
|
|
183
|
+
workspaceId?: string;
|
|
184
|
+
senderId?: string;
|
|
185
|
+
targetDate?: string;
|
|
186
|
+
lane?: string;
|
|
187
|
+
campaignId?: string;
|
|
188
|
+
requestedCount?: number;
|
|
189
|
+
idempotencyKey?: string;
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* Place ready outreach for ONE explicit workspace scope.
|
|
193
|
+
*
|
|
194
|
+
* The returned value is the plain canonical business receipt. Every failure that
|
|
195
|
+
* is NOT a decoded business fact — a refused request, an authorization refusal,
|
|
196
|
+
* an infrastructure failure, or a response the sole decoder rejects — propagates
|
|
197
|
+
* as a thrown transport error and can never become a plausible mutation receipt.
|
|
198
|
+
*/
|
|
199
|
+
export declare function fillRefillV3ReadyTool(input: RefillV3FillReadyToolInput): Promise<RefillV3FillReadyResult>;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Refill V3 fill-ready PUBLIC MCP projection (Phase 146.5 Plan 08, RV3MCP-002).
|
|
3
|
+
*
|
|
4
|
+
* This is a THIN projection over Plan 07's package-root contract and nothing
|
|
5
|
+
* else:
|
|
6
|
+
*
|
|
7
|
+
* 1. it mirrors Plan 07's closed seven-key request vocabulary so a
|
|
8
|
+
* protocol-level validator refuses drift — including the alias `count` —
|
|
9
|
+
* before the handler runs;
|
|
10
|
+
* 2. it applies the ONE shared explicit-workspace guard, because a bounded
|
|
11
|
+
* mutation must name its target workspace instead of switching the shared
|
|
12
|
+
* active workspace;
|
|
13
|
+
* 3. it posts the caller's exact values once to the authenticated route, whose
|
|
14
|
+
* `parseRefillV3FillReadyInput` is the SOLE runtime authority; and
|
|
15
|
+
* 4. it returns only what Plan 07's SOLE closed decoder produced.
|
|
16
|
+
*
|
|
17
|
+
* It holds no approval, scheduler, cohort, clamp, or idempotency policy: the
|
|
18
|
+
* caller's replay identity is forwarded byte-for-byte, because rewriting it
|
|
19
|
+
* would silently turn a replay into a second mutation scope.
|
|
20
|
+
*
|
|
21
|
+
* A typed business blocker — including `blocked`, `campaign_exhausted`, and
|
|
22
|
+
* `awaiting_scheduler` — is a successful response VALUE and passes through
|
|
23
|
+
* VERBATIM through the central `toMcpToolResult`. Transport failure is the ONLY
|
|
24
|
+
* source of an error result (error law 6), so this module contains no branch
|
|
25
|
+
* that reads a decoded business field to decide failure.
|
|
26
|
+
*
|
|
27
|
+
* The declarative input schema is mirrored here rather than imported because the
|
|
28
|
+
* published package must never import app `src/**`. The mirror is not trusted:
|
|
29
|
+
* `tests/mcp/refill-v3/fill-ready-tool.test.ts` asserts its required key set and
|
|
30
|
+
* property set equal Plan 07's `REFILL_V3_FILL_READY_INPUT_KEYS`, so drift is a
|
|
31
|
+
* failing pin.
|
|
32
|
+
*/
|
|
33
|
+
import { getApi } from "../api.js";
|
|
34
|
+
import { decodeRefillV3FillReadyResult, REFILL_V3_FILL_READY_LANES, REFILL_V3_FILL_READY_OUTPUT_SCHEMA, } from "./refill-v3-fill-ready-contract.js";
|
|
35
|
+
import { normalizeExplicitWorkspaceId, workspaceRequestOptions, } from "./workspace-context.js";
|
|
36
|
+
/** The one authenticated backend boundary this projection may reach. */
|
|
37
|
+
export const REFILL_V3_FILL_READY_ROUTE = "/api/v3/mcp/refill-v3/fill-ready";
|
|
38
|
+
export const REFILL_V3_FILL_READY_TOOL_NAME = "refill_v3_fill_ready";
|
|
39
|
+
/** Declarative mirror of Plan 07's closed seven-key request vocabulary. */
|
|
40
|
+
const REFILL_V3_FILL_READY_TOOL_INPUT_SCHEMA = {
|
|
41
|
+
type: "object",
|
|
42
|
+
additionalProperties: false,
|
|
43
|
+
required: [
|
|
44
|
+
"workspaceId",
|
|
45
|
+
"senderId",
|
|
46
|
+
"targetDate",
|
|
47
|
+
"lane",
|
|
48
|
+
"campaignId",
|
|
49
|
+
"requestedCount",
|
|
50
|
+
"idempotencyKey",
|
|
51
|
+
],
|
|
52
|
+
properties: {
|
|
53
|
+
workspaceId: {
|
|
54
|
+
type: "string",
|
|
55
|
+
minLength: 1,
|
|
56
|
+
description: "Explicit request-scoped workspace id. Pass this instead of switching the shared active workspace.",
|
|
57
|
+
},
|
|
58
|
+
senderId: {
|
|
59
|
+
type: "string",
|
|
60
|
+
minLength: 1,
|
|
61
|
+
description: "Exact Sender.id. Never pass a sender name.",
|
|
62
|
+
},
|
|
63
|
+
targetDate: {
|
|
64
|
+
type: "string",
|
|
65
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}$",
|
|
66
|
+
description: "The sender-local calendar date to fill, YYYY-MM-DD.",
|
|
67
|
+
},
|
|
68
|
+
lane: {
|
|
69
|
+
type: "string",
|
|
70
|
+
enum: [...REFILL_V3_FILL_READY_LANES],
|
|
71
|
+
description: "The exact lane to fill. The two lanes are independent.",
|
|
72
|
+
},
|
|
73
|
+
campaignId: {
|
|
74
|
+
type: "string",
|
|
75
|
+
minLength: 1,
|
|
76
|
+
description: "Exact id of the ONE campaign this request may touch, chosen from the world-state options.",
|
|
77
|
+
},
|
|
78
|
+
requestedCount: {
|
|
79
|
+
type: "integer",
|
|
80
|
+
minimum: 0,
|
|
81
|
+
description: "How many actions to place. The backend clamps it to real capacity, supply, and clean candidates and names the binding limit; 0 is a valid bounded no-op.",
|
|
82
|
+
},
|
|
83
|
+
idempotencyKey: {
|
|
84
|
+
type: "string",
|
|
85
|
+
minLength: 1,
|
|
86
|
+
description: "Caller replay identity. Reuse the SAME key to retry one request; a new key means a new placement scope.",
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
export const refillV3FillReadyToolDefinitions = [
|
|
91
|
+
{
|
|
92
|
+
name: REFILL_V3_FILL_READY_TOOL_NAME,
|
|
93
|
+
description: "Place just-in-time ready outreach for exactly one workspace, sender, sender-local date, lane, and campaign. It MUTATES product state: within the exact bounded cohort it approves prepared rows that still need approval, assigns sender affinity, and schedules ready actions through the on-demand scheduler. The two lanes connection_invite and paid_inmail are independent and a paid request re-attests paid-InMail credits immediately before placement. It never creates rows, imports leads, generates messages, starts or launches a campaign, changes thresholds, or sends anything directly. Every attempt is bounded by the required idempotencyKey: reuse the SAME idempotencyKey to retry one request, and expect a closed terminal outcome of scheduled, partially_scheduled, awaiting_scheduler, campaign_exhausted, or blocked, each with a typed reason and named clamp. An awaiting_scheduler answer carries a pollKey whose only valid follow-up is a status lookup, never a second placement.",
|
|
94
|
+
inputSchema: REFILL_V3_FILL_READY_TOOL_INPUT_SCHEMA,
|
|
95
|
+
outputSchema: REFILL_V3_FILL_READY_OUTPUT_SCHEMA,
|
|
96
|
+
annotations: {
|
|
97
|
+
title: "Refill V3 Fill Ready",
|
|
98
|
+
readOnlyHint: false,
|
|
99
|
+
// Destructive in the PRODUCT-STATE sense: approval and scheduling change
|
|
100
|
+
// durable customer state that a caller cannot undo from this tool.
|
|
101
|
+
destructiveHint: true,
|
|
102
|
+
idempotentHint: true,
|
|
103
|
+
openWorldHint: false,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
/**
|
|
108
|
+
* Place ready outreach for ONE explicit workspace scope.
|
|
109
|
+
*
|
|
110
|
+
* The returned value is the plain canonical business receipt. Every failure that
|
|
111
|
+
* is NOT a decoded business fact — a refused request, an authorization refusal,
|
|
112
|
+
* an infrastructure failure, or a response the sole decoder rejects — propagates
|
|
113
|
+
* as a thrown transport error and can never become a plausible mutation receipt.
|
|
114
|
+
*/
|
|
115
|
+
export async function fillRefillV3ReadyTool(input) {
|
|
116
|
+
const workspaceId = normalizeExplicitWorkspaceId(input.workspaceId);
|
|
117
|
+
if (!workspaceId) {
|
|
118
|
+
throw new Error("Explicit workspaceId is required for refill_v3_fill_ready. Name the target workspace instead of switching the shared active workspace.");
|
|
119
|
+
}
|
|
120
|
+
const raw = await getApi().post(REFILL_V3_FILL_READY_ROUTE, {
|
|
121
|
+
workspaceId,
|
|
122
|
+
senderId: input.senderId,
|
|
123
|
+
targetDate: input.targetDate,
|
|
124
|
+
lane: input.lane,
|
|
125
|
+
campaignId: input.campaignId,
|
|
126
|
+
requestedCount: input.requestedCount,
|
|
127
|
+
// Forwarded byte-for-byte: rewriting the caller's replay identity would
|
|
128
|
+
// turn a retry into a second placement scope.
|
|
129
|
+
idempotencyKey: input.idempotencyKey,
|
|
130
|
+
}, workspaceRequestOptions(workspaceId));
|
|
131
|
+
const decoded = decodeRefillV3FillReadyResult(raw);
|
|
132
|
+
if (decoded === null) {
|
|
133
|
+
throw new Error("The Refill V3 fill-ready response failed the published result contract.");
|
|
134
|
+
}
|
|
135
|
+
return decoded;
|
|
136
|
+
}
|