@volter/twin-moonshot 0.1.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.
Files changed (39) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +164 -0
  3. package/dist/src/cli.d.ts +2 -0
  4. package/dist/src/cli.js +25 -0
  5. package/dist/src/index.d.ts +14 -0
  6. package/dist/src/index.js +86 -0
  7. package/dist/src/moonshot-budget.d.ts +57 -0
  8. package/dist/src/moonshot-budget.js +142 -0
  9. package/dist/src/moonshot-capabilities.d.ts +4 -0
  10. package/dist/src/moonshot-capabilities.js +1200 -0
  11. package/dist/src/moonshot-conformance.d.ts +14 -0
  12. package/dist/src/moonshot-conformance.js +405 -0
  13. package/dist/src/moonshot-connector.d.ts +168 -0
  14. package/dist/src/moonshot-connector.js +416 -0
  15. package/dist/src/moonshot-models.d.ts +36 -0
  16. package/dist/src/moonshot-models.js +37 -0
  17. package/dist/src/moonshot-scenario.d.ts +54 -0
  18. package/dist/src/moonshot-scenario.js +175 -0
  19. package/dist/src/moonshot-server.d.ts +13 -0
  20. package/dist/src/moonshot-server.js +202 -0
  21. package/dist/src/moonshot-stub.d.ts +70 -0
  22. package/dist/src/moonshot-stub.js +222 -0
  23. package/dist/src/moonshot-twin.d.ts +144 -0
  24. package/dist/src/moonshot-twin.js +1647 -0
  25. package/dist/src/moonshot-types.d.ts +251 -0
  26. package/dist/src/moonshot-types.js +19 -0
  27. package/package.json +53 -0
  28. package/src/cli.ts +25 -0
  29. package/src/index.ts +129 -0
  30. package/src/moonshot-budget.ts +163 -0
  31. package/src/moonshot-capabilities.ts +1220 -0
  32. package/src/moonshot-conformance.ts +416 -0
  33. package/src/moonshot-connector.ts +465 -0
  34. package/src/moonshot-models.ts +89 -0
  35. package/src/moonshot-scenario.ts +194 -0
  36. package/src/moonshot-server.ts +220 -0
  37. package/src/moonshot-stub.ts +230 -0
  38. package/src/moonshot-twin.ts +1670 -0
  39. package/src/moonshot-types.ts +225 -0
@@ -0,0 +1,416 @@
1
+ // Moonshot CONNECTOR — the live-vendor pull/push path that gives the Moonshot twin the full
2
+ // "git for SaaS" lifecycle (pull real state → mirror; push local writes → real).
3
+ //
4
+ // PROTOCOL 2 (docs/contributing/architecture.md#protocol-2-the-pack-is-a-plugin): the pack is a plugin — its wire, its tree, and its half of
5
+ // the real state system.
6
+ // REFRESH (real → twin): fetch the real Models / Files / Batches / Balance, map them to
7
+ // observed resources, and fold them onto the root's log through the
8
+ // kernel's `observeResources` (content-addressed entries, so re-pulling
9
+ // identical state appends nothing).
10
+ // PERFORM (twin → real): the HEAD calls `performMoonshotAction` per deployable entry; the
11
+ // vendor's minted id comes back as the `externalId` the kernel adopts.
12
+ //
13
+ // The vendor I/O is an INJECTED executor (B3 auth boundary): the kernel + this pack hold NO
14
+ // Moonshot key and import NO SDK at runtime. Tests pass a fake executor; live runs pass
15
+ // `liveMoonshotExecute(apiKey)`. Same code path either way — fully exercisable offline.
16
+ import { assertBudgetGuardIntact, confirmAction, deployableEntries, observeResources, resolveSubjectId } from '@volter/world-core';
17
+ import { MoonshotBudget, MoonshotBudgetError, moonshotCallWeight } from "./moonshot-budget.js";
18
+ const SERVICE = 'moonshot';
19
+ /**
20
+ * A live executor against the real Moonshot REST API (the user's own API key). Sends the required
21
+ * `Authorization: Bearer` header. Never imported by the pack's own serve path — only constructed
22
+ * by a caller that opts into real I/O.
23
+ *
24
+ * THIS IS THE ONE PLACE this pack issues a live `api.moonshot.ai` request, and therefore the one
25
+ * place the rate budget has to be enforced. EVERY call is guarded: the budget is charged BEFORE
26
+ * the request goes out (`checkBudget`, which THROWS `MoonshotBudgetError` instead of returning
27
+ * when the ceiling or a cooldown says stop) and the response is fed back (`recordCall`) so a
28
+ * `retry-after` / 429 / `x-ratelimit-remaining: 0` signal becomes a persisted cooldown that makes
29
+ * every later call fail fast WITHOUT touching Moonshot. There is deliberately no OPTION to
30
+ * disable the guard, and no value a caller can pass for `budget` that yields an unguarded client.
31
+ * What that does NOT claim is immunity from a caller who WANTS one: a fresh `budgetOptions.path`
32
+ * per construction, or an injected clock, restores the allowance, because the same seam tests
33
+ * need cannot be denied to a determined caller in the same process. See `moonshot-budget.ts` and
34
+ * the kernel header for the limits of the guarantee.
35
+ */
36
+ export function liveMoonshotExecute(apiKey, base = 'https://api.moonshot.ai', opts = {}) {
37
+ const doFetch = opts.fetchImpl ?? fetch;
38
+ // ONE expression decides which budget is used, so there is no second, weaker test that could
39
+ // disagree with the first. `null`/`undefined` (or omitting it) build the default; anything else
40
+ // must be an UNMODIFIED MoonshotBudget — a duck-typed stand-in, a SUBCLASS overriding
41
+ // `checkBudget`, and a Proxy trapping it are ALL refused, because each is a one-liner that
42
+ // would otherwise hand back a client with no ceiling. The default ledger is keyed by a hash of
43
+ // THIS key: Moonshot limits per account, so a cwd-scoped ledger would hand the same key a
44
+ // fresh allowance per checkout/worktree/CI leg.
45
+ const budget = opts.budget !== undefined && opts.budget !== null
46
+ ? assertBudgetGuardIntact(opts.budget, MoonshotBudget, 'liveMoonshotExecute')
47
+ : new MoonshotBudget({ token: apiKey, ...(opts.budgetOptions ?? {}) });
48
+ return async (method, path, body) => {
49
+ // A path this pack never modelled is refused BEFORE the budget is even charged: an executor
50
+ // that will happily issue any URL is how a careless script reaches an unpriced endpoint.
51
+ if (!path.startsWith('/v1/') && !path.startsWith('/anthropic/v1/')) {
52
+ throw new Error(`moonshot: refusing to call an unmodeled path "${path}" — Moonshot serves /v1 and /anthropic/v1`);
53
+ }
54
+ const headers = { authorization: `Bearer ${apiKey}` };
55
+ const init = { method, headers };
56
+ if (method === 'POST') {
57
+ headers['content-type'] = 'application/json';
58
+ init.body = JSON.stringify(body ?? {});
59
+ }
60
+ const weight = moonshotCallWeight(method, path);
61
+ // THROWS instead of calling. Nothing below this line runs when the budget refuses.
62
+ const reservation = budget.checkBudget(weight);
63
+ const res = await doFetch(`${base}${path}`, init);
64
+ const resHeaders = {};
65
+ res.headers.forEach((v, k) => { resHeaders[k.toLowerCase()] = v; });
66
+ const parsed = (await res.json());
67
+ // Settles the reservation and, on a back-off signal, arms the cooldown.
68
+ // recordCall may THROW after arming the cooldown (a back-off beyond the cap). On a refused
69
+ // call that louder refusal wins; an answer Moonshot ACCEPTED is kept, so a write that landed is
70
+ // never recorded as failed and performed again on retry.
71
+ try {
72
+ budget.recordCall(weight, resHeaders, { status: res.status, reservation });
73
+ }
74
+ catch (error) {
75
+ if (!(error instanceof MoonshotBudgetError) || !res.ok)
76
+ throw error;
77
+ }
78
+ return parsed;
79
+ };
80
+ }
81
+ function listOf(res) {
82
+ return Array.isArray(res.data) ? res.data : [];
83
+ }
84
+ /**
85
+ * A REFUSED pull is NOT an empty account. Moonshot answers failures with its documented
86
+ * `{ error: { message, type } }` envelope, so a status check alone is not enough — throw on the
87
+ * envelope rather than folding an empty list over real observed state.
88
+ */
89
+ function throwIfError(res, ctx) {
90
+ if (res.error)
91
+ throw new Error(`moonshot ${ctx} failed: ${res.error.message ?? res.error.type ?? 'unknown error'}`);
92
+ }
93
+ // ── PULL ────────────────────────────────────────────────────────────────────
94
+ /** Map a real-Moonshot model object → a twin sync resource. */
95
+ export function mapModel(m) {
96
+ return {
97
+ type: 'model',
98
+ id: String(m.id),
99
+ fields: {
100
+ object: 'model',
101
+ created: m.created ?? null,
102
+ owned_by: m.owned_by ?? null,
103
+ },
104
+ };
105
+ }
106
+ /** Map a real-Moonshot File object → a twin sync resource. */
107
+ export function mapFile(f) {
108
+ return {
109
+ type: 'file',
110
+ id: String(f.id),
111
+ fields: {
112
+ object: 'file',
113
+ bytes: f.bytes ?? 0,
114
+ created_at: f.created_at ?? null,
115
+ filename: f.filename ?? null,
116
+ purpose: f.purpose ?? null,
117
+ status: f.status ?? null,
118
+ },
119
+ };
120
+ }
121
+ /** Map a real-Moonshot Batch object → a twin sync resource. */
122
+ export function mapBatch(b) {
123
+ const counts = (b.request_counts && typeof b.request_counts === 'object') ? b.request_counts : {};
124
+ return {
125
+ type: 'batch',
126
+ id: String(b.id),
127
+ fields: {
128
+ object: 'batch',
129
+ endpoint: b.endpoint ?? null,
130
+ input_file_id: b.input_file_id ?? null,
131
+ completion_window: b.completion_window ?? null,
132
+ status: b.status ?? null,
133
+ output_file_id: b.output_file_id ?? null,
134
+ created_at: b.created_at ?? null,
135
+ completed_at: b.completed_at ?? null,
136
+ request_counts: counts,
137
+ },
138
+ };
139
+ }
140
+ /** Map the real-Moonshot balance → the twin's 'balance' resource (id 'me'). */
141
+ export function mapBalance(b) {
142
+ const d = (b.data && typeof b.data === 'object') ? b.data : {};
143
+ return {
144
+ type: 'balance',
145
+ id: 'me',
146
+ fields: {
147
+ available_balance: d.available_balance ?? null,
148
+ voucher_balance: d.voucher_balance ?? null,
149
+ cash_balance: d.cash_balance ?? null,
150
+ },
151
+ };
152
+ }
153
+ const COLLECTIONS = [
154
+ { path: '/v1/models', map: mapModel },
155
+ { path: '/v1/files', map: mapFile },
156
+ { path: '/v1/batches', map: mapBatch },
157
+ ];
158
+ /** Pull all modeled real collections via the executor and map them to twin sync resources. */
159
+ export async function pullMoonshotState(execute) {
160
+ const out = [];
161
+ for (const c of COLLECTIONS) {
162
+ const res = await execute('GET', c.path);
163
+ throwIfError(res, `pull ${c.path}`);
164
+ for (const item of listOf(res))
165
+ out.push(c.map(item));
166
+ }
167
+ // The balance is its own envelope ({code, data:{…}, scode, status}) — no `data` list.
168
+ const bal = await execute('GET', '/v1/users/me/balance');
169
+ throwIfError(bal, 'pull /v1/users/me/balance');
170
+ out.push(mapBalance(bal));
171
+ return out;
172
+ }
173
+ /**
174
+ * Pull from real Moonshot and fold into the twin (mirror seeding). Observation entries are
175
+ * content-addressed, so a re-pull of identical state appends nothing.
176
+ *
177
+ * `occurredAt` has NO pinned default on purpose: the kernel hashes an observed event over
178
+ * (occurredAt + post-state), so under a fixed poll time a vendor value that REVERTS across polls
179
+ * collides with its own earlier observation and the fold reports a phantom delta while the
180
+ * projection keeps the stale value (ADDING_A_TWIN.md §6). Callers pass a moving timestamp.
181
+ */
182
+ export async function syncMoonshotFromReal(execute, opts) {
183
+ const resources = await pullMoonshotState(execute);
184
+ const result = observeResources(SERVICE, resources.map((r) => ({ type: r.type, id: r.id, fields: r.fields })), {
185
+ ...(opts.root !== undefined ? { root: opts.root } : {}), at: opts.occurredAt, batch: `obs:${SERVICE}:${opts.occurredAt}`,
186
+ });
187
+ return { observed: result.observed, deltasAppended: result.appended };
188
+ }
189
+ // ── PUSH ────────────────────────────────────────────────────────────────────
190
+ // The twin operations this connector knows how to push to real Moonshot. Anything not here must
191
+ // FAIL LOUDLY rather than be silently dropped — pushing an unrecognized op risks hitting the
192
+ // wrong endpoint or no-op'ing a real change.
193
+ const PUSHABLE_VERBS = new Set(['create', 'cancel', 'delete']);
194
+ /** Throw if `op` is not a write operation this connector can faithfully push. */
195
+ function assertPushable(op) {
196
+ const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
197
+ if (!PUSHABLE_VERBS.has(verb)) {
198
+ throw new Error(`moonshot push: unsupported operation '${op}' — refusing to silently drop a local write`);
199
+ }
200
+ const why = UNPUSHABLE[op];
201
+ if (why)
202
+ throw new Error(`moonshot push: cannot push '${op}' — ${why}`);
203
+ }
204
+ /** Why this action cannot be pushed, or null if it can. Pure — no vendor call on its path. */
205
+ export function unpushableReason(op) {
206
+ const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
207
+ if (!PUSHABLE_VERBS.has(verb))
208
+ return `unsupported operation '${op}'`;
209
+ return UNPUSHABLE[op] ?? null;
210
+ }
211
+ // Map a subject type → its REST collection path.
212
+ const COLLECTION_PATH = {
213
+ file: '/v1/files',
214
+ batch: '/v1/batches',
215
+ };
216
+ /**
217
+ * Pushes this connector must REFUSE rather than fake, keyed `"<type>.<verb>"`.
218
+ *
219
+ * `file.create` is the case: real Moonshot's `POST /v1/files` is multipart/form-data carrying
220
+ * the actual file body, while this executor sends JSON. Pushing `{purpose, filename}` as JSON
221
+ * would 400 at the vendor, and the twin stores no binary content to upload anyway. Refusing
222
+ * loudly is the honest answer; the gap is filed as `moonshot.connector.push_file_create`.
223
+ */
224
+ const UNPUSHABLE = {
225
+ 'file.create': "real Moonshot's POST /v1/files is multipart/form-data with the file body; this JSON executor cannot express it, and the twin stores no real bytes to send",
226
+ };
227
+ /**
228
+ * The twin's LOCALLY-MINTED id namespace (`file_twin_1`, `batch_twin_3` — see `nextId` in
229
+ * moonshot-twin.ts). A subject still bearing one has no counterpart in the real account.
230
+ */
231
+ const LOCAL_ID = /_twin_\d+$/;
232
+ /**
233
+ * Resolve the id the VENDOR knows this subject by.
234
+ *
235
+ * The kernel records the vendor's minted id at landing time (`vendorSubjectId` → the landed copy
236
+ * carries the vendor's id, `aliasOf` the local one), and `resolveSubjectId` answers it — so a
237
+ * later delete/cancel addresses the vendor by THAT. A locally-minted subject with no recorded
238
+ * alias is refused rather than guessed at — addressing the real account by the twin's own mint
239
+ * would DELETE or CANCEL a resource the vendor never had.
240
+ */
241
+ export function externalIdFor(subjectType, subjectId, root) {
242
+ if (!LOCAL_ID.test(subjectId))
243
+ return subjectId; // already a vendor id (e.g. observed by a pull)
244
+ const resolved = resolveSubjectId(SERVICE, subjectType, subjectId, root);
245
+ return resolved !== subjectId ? resolved : null;
246
+ }
247
+ /**
248
+ * Resolve the REST (method, path) for ONE pending action — faithful to the real Moonshot REST
249
+ * surface:
250
+ * - <type>.create → POST <collection>
251
+ * - <type>.cancel → POST <collection>/:id/cancel
252
+ * - <type>.delete → DELETE <collection>/:id
253
+ */
254
+ export function moonshotRequestForAction(action,
255
+ /** The id the VENDOR knows this subject by. Required for anything but a create — see
256
+ * `externalIdFor`; passing the twin's own mint would address a resource the vendor never had. */
257
+ externalId) {
258
+ const op = action.operation ?? `${action.subject.type}.update`;
259
+ const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
260
+ const collection = COLLECTION_PATH[action.subject.type];
261
+ if (!collection)
262
+ throw new Error(`moonshot push: no REST collection for subject type '${action.subject.type}'`);
263
+ if (verb === 'create')
264
+ return { method: 'POST', path: collection };
265
+ const vendorId = externalId ?? action.subject.id;
266
+ if (LOCAL_ID.test(vendorId)) {
267
+ throw new Error(`moonshot push: refusing to address the real account by the twin's own id '${vendorId}' — no vendor id was ever recorded for this subject`);
268
+ }
269
+ if (verb === 'cancel')
270
+ return { method: 'POST', path: `${collection}/${vendorId}/cancel` };
271
+ if (verb === 'delete')
272
+ return { method: 'DELETE', path: `${collection}/${vendorId}` };
273
+ // assertPushable rejects anything else, so this is only reached for the pushable verbs above.
274
+ return { method: 'POST', path: collection };
275
+ }
276
+ /**
277
+ * Push ONE pending action to REAL Moonshot via the injected executor. Returns the real external
278
+ * id (the object id from the response; for a create that's a freshly minted id, otherwise it
279
+ * echoes the subject). WRITES TO THE REAL ACCOUNT.
280
+ */
281
+ export async function pushMoonshotAction(execute, action, opts = {}) {
282
+ assertPushable(action.operation ?? `${action.subject.type}.update`);
283
+ const { method, path } = moonshotRequestForAction(action, opts.externalId);
284
+ const verb = (action.operation ?? '').includes('.') ? action.operation.slice(action.operation.indexOf('.') + 1) : '';
285
+ const payload = verb === 'create' ? createPayload(action) : undefined;
286
+ const res = await execute(method, path, payload);
287
+ throwIfError(res, `push ${action.subject.type}`);
288
+ const id = res.id;
289
+ return { externalId: typeof id === 'string' && id ? id : action.subject.id };
290
+ }
291
+ function createPayload(action) {
292
+ const f = (action.fields ?? {});
293
+ switch (action.subject.type) {
294
+ case 'batch':
295
+ return { input_file_id: f.input_file_id, endpoint: f.endpoint, completion_window: f.completion_window, ...(f.metadata ? { metadata: f.metadata } : {}) };
296
+ default:
297
+ return {};
298
+ }
299
+ }
300
+ // ── PROTOCOL 2: the pack's half of the real state system ────────────────────────────────────
301
+ /** The pack's executor over the kernel's: the same Moonshot call, carried by the head. The
302
+ * credential never enters this file — the kernel's executor applies it to the authorization
303
+ * header before this sees the request. */
304
+ export function moonshotExecuteOver(execute) {
305
+ return async (method, path, body) => {
306
+ const res = await execute({
307
+ method, path,
308
+ headers: { accept: 'application/json', authorization: 'Bearer twin', ...(method === 'POST' ? { 'content-type': 'application/json' } : {}) },
309
+ ...(body === undefined ? {} : { body: JSON.stringify(body) }),
310
+ });
311
+ if (res.body === '')
312
+ return {};
313
+ try {
314
+ return JSON.parse(res.body);
315
+ }
316
+ catch {
317
+ return { error: { type: 'server_error', message: res.body.slice(0, 200) } };
318
+ }
319
+ };
320
+ }
321
+ /** The refresh adapter: pull the account's models / files / batches / balance through the executor. */
322
+ export async function syncMoonshotFromRemote(execute, opts = {}) {
323
+ return syncMoonshotFromReal(moonshotExecuteOver(execute), {
324
+ ...(opts.root !== undefined ? { root: opts.root } : {}),
325
+ occurredAt: opts.occurredAt ?? new Date().toISOString(),
326
+ });
327
+ }
328
+ /**
329
+ * The PERFORM adapter: one deployable entry crosses to Moonshot, or settles with the reason it
330
+ * never could. `file.create` is unpushable-BY-DESIGN (the real endpoint is multipart with the
331
+ * file body; this executor sends JSON) — it settles without crossing rather than throwing, so it
332
+ * cannot wedge every later batch action behind it. A locally-minted subject with no vendor alias
333
+ * THROWS (the head records the failure): addressing the real account by the twin's own mint would
334
+ * delete or cancel a resource the vendor never had.
335
+ */
336
+ export async function performMoonshotAction(execute, action, ctx) {
337
+ const op = action.operation ?? `${action.subject.type}.update`;
338
+ const why = unpushableReason(op);
339
+ if (why)
340
+ return { externalId: action.subject.id, data: { performed: false, reason: why } };
341
+ const vendorId = ctx.resolve(action.subject.type, action.subject.id);
342
+ const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
343
+ if (verb !== 'create' && LOCAL_ID.test(vendorId)) {
344
+ throw new Error(`moonshot push: refusing to address the real account by the twin's own id '${vendorId}' — no vendor id was ever recorded for this subject`);
345
+ }
346
+ const { externalId } = await pushMoonshotAction(moonshotExecuteOver(execute), { operation: op, subject: action.subject, fields: action.fields ?? {} }, ...(verb !== 'create' ? [{ externalId: vendorId }] : []));
347
+ return { externalId };
348
+ }
349
+ /**
350
+ * Push the twin's PENDING local actions to real Moonshot and CONFIRM each. Idempotency: a
351
+ * confirmed action is no longer deployable, so a re-push enacts NOTHING.
352
+ *
353
+ * Every action this pack records is SINGLE-RESOURCE (one file, one batch), so confirming with
354
+ * `fields` alone is correct — see ADDING_A_TWIN.md §5 on compound actions, which this pack has
355
+ * none of. If a compound write is ever added here, it must ride its extra resources in
356
+ * `additionalObservations` or the push will silently delete them.
357
+ */
358
+ export async function pushPendingMoonshotActions(execute, opts) {
359
+ const confirmed = [];
360
+ const externalIds = {};
361
+ const refused = [];
362
+ for (const action of deployableEntries(SERVICE, opts.root)) {
363
+ const op = action.operation ?? `${action.subject.type}.update`;
364
+ // An action this connector cannot faithfully push is SKIPPED AND REPORTED, never confirmed
365
+ // and never silently dropped: it stays deployable, and it is named in `refused` so a caller
366
+ // can see it. Throwing instead meant ONE unpushable action — e.g. the local file create,
367
+ // whose real endpoint is multipart — aborted the whole sweep and took every unrelated
368
+ // pending write down with it.
369
+ const why = unpushableReason(op);
370
+ if (why) {
371
+ refused.push({ actionId: action.id, operation: op, reason: why });
372
+ continue;
373
+ }
374
+ const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
375
+ // Anything but a create must address the vendor by the id the vendor issued. A locally-minted
376
+ // subject with none recorded is REFUSED, never guessed.
377
+ let external;
378
+ if (verb !== 'create') {
379
+ const resolved = externalIdFor(action.subject.type, action.subject.id, opts.root);
380
+ if (resolved === null) {
381
+ refused.push({ actionId: action.id, operation: op, reason: `no vendor id recorded for ${action.subject.type} '${action.subject.id}' — it was never pushed to the real account, so there is nothing there to ${verb}` });
382
+ continue;
383
+ }
384
+ external = resolved;
385
+ }
386
+ const { externalId } = await pushMoonshotAction(execute, action, ...(external !== undefined ? [{ externalId: external }] : []));
387
+ // The kernel records the vendor's id as the subject ALIAS (the landed copy carries the
388
+ // vendor's id, aliasOf the local one), so a later delete/cancel resolves through
389
+ // `resolveSubjectId` — no `_external_id` field to read back.
390
+ confirmAction({
391
+ service: SERVICE, actionId: action.id, subject: action.subject,
392
+ fields: action.fields ?? {},
393
+ occurredAt: opts.occurredAt, vendorSubjectId: externalId, receipt: { status: 'deployed' },
394
+ ...(opts.root !== undefined ? { root: opts.root } : {}),
395
+ });
396
+ confirmed.push(action.id);
397
+ externalIds[action.id] = externalId;
398
+ }
399
+ return { pushed: confirmed.length, confirmed, externalIds, refused };
400
+ }
401
+ // ── FULL bi-directional sync ─────────────────────────────────────────────────
402
+ /**
403
+ * FULL bi-directional sync over the injected client: (1) PUSH every deployable local entry to
404
+ * real Moonshot and confirm it, then (2) PULL all modeled collections back and fold them onto
405
+ * the root's log. Pushing first means the pull observes the twin's own writes as confirmed
406
+ * external state (no double-count). Re-running with nothing deployable and identical real state
407
+ * is a no-op.
408
+ */
409
+ export async function fullSyncMoonshot(execute, opts) {
410
+ const push = await pushPendingMoonshotActions(execute, { occurredAt: opts.occurredAt, ...(opts.root !== undefined ? { root: opts.root } : {}) });
411
+ const resources = await pullMoonshotState(execute);
412
+ const pull = observeResources(SERVICE, resources.map((r) => ({ type: r.type, id: r.id, fields: r.fields })), {
413
+ ...(opts.root !== undefined ? { root: opts.root } : {}), at: opts.occurredAt, batch: `obs:${SERVICE}:${opts.occurredAt}`,
414
+ });
415
+ return { pushed: push.pushed, observed: pull.observed, deltasAppended: pull.appended, collections: COLLECTIONS.length + 1, refused: push.refused };
416
+ }
@@ -0,0 +1,36 @@
1
+ export type MoonshotModel = {
2
+ id: string;
3
+ object: 'model';
4
+ created: number;
5
+ owned_by: string;
6
+ /** The OpenAPI request schema's enum for this id — which endpoints accept it. */
7
+ supports: {
8
+ /** POST /v1/chat/completions (all four). */
9
+ chat: boolean;
10
+ /** POST /v1/responses — kimi-k3 only ("This endpoint currently supports kimi-k3"). */
11
+ responses: boolean;
12
+ /** POST /anthropic/v1/messages — kimi-k3 only (MessagesRequest['model'] enum). */
13
+ messages: boolean;
14
+ /** POST /v1/batches — only kimi-k2.7-code(-highspeed) and kimi-k2.6, NOT kimi-k3. */
15
+ batch: boolean;
16
+ /** Vision (image_url parts in chat messages). kimi-k3 and kimi-k2.6 are vision models. */
17
+ vision: boolean;
18
+ };
19
+ context_length: number;
20
+ /** Max output tokens. kimi-k3: default 131072, up to 1048576 (ChatRequestCommon
21
+ * max_completion_tokens description). The k2.6/k2.7-code models share the 256k window. */
22
+ max_completion_tokens: number;
23
+ };
24
+ export declare const MOONSHOT_MODELS: MoonshotModel[];
25
+ /** Resolve a model by id, or undefined if the twin doesn't model it. */
26
+ export declare function findModel(id: string): MoonshotModel | undefined;
27
+ /** The ids that accept a batch job's chat/completions requests. */
28
+ export declare const BATCH_MODELS: Set<string>;
29
+ /** kimi-k3's `reasoning_effort` — a CLOSED documented set (KimiK3ChatRequest). */
30
+ export declare const REASONING_EFFORTS: readonly ["low", "high", "max"];
31
+ export type ReasoningEffort = (typeof REASONING_EFFORTS)[number];
32
+ /** kimi-k2.6 `thinking.type` — enabled and disabled are BOTH supported. */
33
+ export declare const K26_THINKING_TYPES: readonly ["enabled", "disabled"];
34
+ /** kimi-k2.7-code `thinking.type` — ONLY 'enabled'; 'disabled' returns an error (OpenAPI:
35
+ * "Unlike kimi-k2.6, `\"disabled\"` is NOT supported — passing it returns an error."). */
36
+ export declare const K27_THINKING_TYPES: readonly ["enabled"];
@@ -0,0 +1,37 @@
1
+ const K3_CTX = 1_048_576;
2
+ const K2_CTX = 262_144;
3
+ export const MOONSHOT_MODELS = [
4
+ {
5
+ id: 'kimi-k3', object: 'model', created: 1_760_000_000, owned_by: 'moonshot',
6
+ supports: { chat: true, responses: true, messages: true, batch: false, vision: true },
7
+ context_length: K3_CTX, max_completion_tokens: 1_048_576,
8
+ },
9
+ {
10
+ id: 'kimi-k2.7-code', object: 'model', created: 1_760_000_001, owned_by: 'moonshot',
11
+ supports: { chat: true, responses: false, messages: false, batch: true, vision: false },
12
+ context_length: K2_CTX, max_completion_tokens: K2_CTX,
13
+ },
14
+ {
15
+ id: 'kimi-k2.7-code-highspeed', object: 'model', created: 1_760_000_002, owned_by: 'moonshot',
16
+ supports: { chat: true, responses: false, messages: false, batch: true, vision: false },
17
+ context_length: K2_CTX, max_completion_tokens: K2_CTX,
18
+ },
19
+ {
20
+ id: 'kimi-k2.6', object: 'model', created: 1_760_000_003, owned_by: 'moonshot',
21
+ supports: { chat: true, responses: false, messages: false, batch: true, vision: true },
22
+ context_length: K2_CTX, max_completion_tokens: K2_CTX,
23
+ },
24
+ ];
25
+ /** Resolve a model by id, or undefined if the twin doesn't model it. */
26
+ export function findModel(id) {
27
+ return MOONSHOT_MODELS.find((m) => m.id === id);
28
+ }
29
+ /** The ids that accept a batch job's chat/completions requests. */
30
+ export const BATCH_MODELS = new Set(MOONSHOT_MODELS.filter((m) => m.supports.batch).map((m) => m.id));
31
+ /** kimi-k3's `reasoning_effort` — a CLOSED documented set (KimiK3ChatRequest). */
32
+ export const REASONING_EFFORTS = ['low', 'high', 'max'];
33
+ /** kimi-k2.6 `thinking.type` — enabled and disabled are BOTH supported. */
34
+ export const K26_THINKING_TYPES = ['enabled', 'disabled'];
35
+ /** kimi-k2.7-code `thinking.type` — ONLY 'enabled'; 'disabled' returns an error (OpenAPI:
36
+ * "Unlike kimi-k2.6, `\"disabled\"` is NOT supported — passing it returns an error."). */
37
+ export const K27_THINKING_TYPES = ['enabled'];
@@ -0,0 +1,54 @@
1
+ import { type PackScenarioAdapter, type ScenarioDocument, ScenarioEngine } from '@volter/world-core';
2
+ import type { MoonshotMessageParam, MoonshotToolCall } from './moonshot-types.js';
3
+ export type MoonshotScenarioRequest = {
4
+ model: string;
5
+ messages: MoonshotMessageParam[];
6
+ tools?: unknown;
7
+ /** Moonshot-specific: the k2.6/k2.7-code `thinking` object. Scriptable because thinking mode
8
+ * decides whether `reasoning_content` appears on the response. */
9
+ thinking?: {
10
+ type?: string;
11
+ keep?: string | null;
12
+ };
13
+ };
14
+ export type MoonshotScenarioEngine = ScenarioEngine<MoonshotScenarioRequest>;
15
+ export type ScenarioToolCall = {
16
+ name: string;
17
+ arguments: Record<string, unknown>;
18
+ id?: string;
19
+ };
20
+ /**
21
+ * What a moonshot handler may script. `reasoning` is Moonshot-specific (the `reasoning_content`
22
+ * field on the assistant message, and the thinking block on the Messages surface); `error`
23
+ * scripts one of Moonshot's own documented failure envelopes rather than a success.
24
+ */
25
+ export type MoonshotScenarioRespond = {
26
+ text?: string;
27
+ reasoning?: string;
28
+ toolCalls?: ScenarioToolCall | ScenarioToolCall[];
29
+ finishReason?: 'stop' | 'length' | 'tool_calls';
30
+ /** A scripted vendor failure: Moonshot's documented error types (platform.kimi.ai/docs/api/errors). */
31
+ error?: {
32
+ type: 'rate_limit_reached_error' | 'server_unavailable' | 'server_error';
33
+ message?: string;
34
+ };
35
+ };
36
+ export type ScriptedResult = {
37
+ text: string | null;
38
+ reasoning: string | null;
39
+ toolCalls: MoonshotToolCall[];
40
+ finishReason: 'stop' | 'length' | 'tool_calls';
41
+ };
42
+ export declare const moonshotScenarioAdapter: PackScenarioAdapter<MoonshotScenarioRequest>;
43
+ /** Load + STRICTLY validate a scenario document. A malformed file throws at load with what is
44
+ * wrong — never a silent ignore-and-stub (a mis-typed rule falling back is a fake success). */
45
+ export declare function loadMoonshotScenarioDocument(path: string): ScenarioDocument;
46
+ export declare function createMoonshotScenarioEngine(document?: ScenarioDocument): MoonshotScenarioEngine;
47
+ /**
48
+ * Turn a validated `respond` into the pack's own faithful assistant turn.
49
+ *
50
+ * Determinism (CLAUDE.md): the tool-call id is derived from the scripted call's own content and
51
+ * its position in the handler — never a module-level counter, which would make two identical
52
+ * scripted requests answer differently after a restart.
53
+ */
54
+ export declare function realizeMoonshotRespond(respond: MoonshotScenarioRespond): ScriptedResult;