@pouchy_ai/world-sdk 0.28.1 → 0.30.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # @pouchy_ai/world-sdk
2
2
 
3
+ ## 0.30.0
4
+
5
+ - **`SKIP_COST_MODEL_VERSION` / `SKIP_COST_MODEL_DIGEST`** — the cost model's
6
+ identity as of this build, so you can detect that the server regrouped a code
7
+ instead of finding out from your ledger.
8
+
9
+ Asked directly after 0.29.0 shipped: does `skipSpentNoModelCall` track the
10
+ server, and would a regrouping be a major bump? The answers are **no** and
11
+ **we will not promise that**. A published package is a SNAPSHOT — regroup a
12
+ code server-side and a pinned client keeps the old answer forever, with no
13
+ error, no code change of yours and green tests. And a promise to bump is a
14
+ claim about human behaviour with no process behind it; the same shape this
15
+ project already declined to sign for the moderation defaults.
16
+
17
+ So the grouping is published as data instead. `GET /v1/world/openapi` carries
18
+ `x-pouchy-cost-model` (`version`, `digest`, `noModelCall`, `modelMayHaveRun`)
19
+ on the `skippedRoles[].code` schema. Compare it against these two constants
20
+ and alarm on a mismatch — a check that fires on the condition itself rather
21
+ than on our remembering to signal it.
22
+
23
+ The digest covers group MEMBERSHIP only: reordering the enum or rewording a
24
+ comment never moves it; a code changing group always does.
25
+
26
+ ## 0.29.0
27
+
28
+ - **`skippedRoles[].code`** — the machine half of "why this role did not
29
+ speak", as a closed `WorldSkipCode` union, plus `skipSpentNoModelCall(code)`
30
+ for the one derived question every consumer asks of it.
31
+
32
+ `reason` has always been a human sentence and stays one; nothing should key
33
+ off it. This exists because an integrator, given the reason prefixes in a
34
+ letter (the spec published none), built a cost model on
35
+ `reason.startsWith('turn error:')`. Their own words for the exposure: rename
36
+ that prefix and "we would not error — we would quietly file that class as
37
+ costing nothing." The same argument the rejection taxonomy made for
38
+ `rejectedEffects[].code`, applied to the sibling array that missed it.
39
+
40
+ Twelve codes. Eight of them mean no model call was made — `not_focused`,
41
+ `role_cap_reached`, `no_actor_bound`, `deadline_exhausted`, `not_admitted`,
42
+ `actor_unavailable`, `session_busy`, `content_policy`. Two mean a model ran or
43
+ may have: `turn_error` (deliberately does NOT promise a call happened —
44
+ context assembly failing and a provider dying mid-stream both land there) and
45
+ `no_message`. Two are beats where the role DID speak and the line was
46
+ withheld: `effect_refused`, `conflict_undelivered`.
47
+
48
+ Additive and optional: absent from a server predating the taxonomy, which
49
+ reads as unknown, never as a value. `skipSpentNoModelCall` returns false for
50
+ an unknown code and for `turn_error`, on the principle that a cost model
51
+ which under-counts silently is worse than one that over-counts visibly.
52
+
3
53
  ## 0.28.1
4
54
 
5
55
  - **`WorldApiError.errorId`** — the server's lookup reference (`err_…`) for a
package/README.md CHANGED
@@ -121,7 +121,7 @@ answers 422 rather than dropping them quietly.
121
121
  **Focusing a beat on some of the cast.** By default every bound role answers a
122
122
  beat (up to the server cap). For an interview or a one-on-one scene, pass
123
123
  `focusRoles` and only those roles run — the rest are not billed and are
124
- recorded in `skippedRoles` as `'not focused'`:
124
+ recorded in `skippedRoles` with `code: 'not_focused'`:
125
125
 
126
126
  ```ts
127
127
  const beat = await world.runTurn({
@@ -135,6 +135,31 @@ Shrink-only: the server intersects the focus with the bound story cast, so it
135
135
  can never widen a beat, and a focus matching no bound role is refused as
136
136
  `no selectable role` without spending anything.
137
137
 
138
+ **Why a role was passed over.** Each `skippedRoles` entry carries a `reason`
139
+ (the human line, which carries the sub-cause and is free to be reworded) and a
140
+ `code` — a closed `WorldSkipCode`. **Key off the code, never the reason.**
141
+ Reading an error string to decide what happened is how a message edit silently
142
+ reclassifies a whole class; this SDK ships the enum precisely so you do not
143
+ have to pattern-match prose.
144
+
145
+ ```ts
146
+ import { skipSpentNoModelCall } from '@pouchy_ai/world-sdk';
147
+
148
+ for (const s of beat.skippedRoles) {
149
+ if (s.code === undefined) audit.unknown(s.roleId, s.reason); // old server
150
+ else if (skipSpentNoModelCall(s.code)) cost.free(s.roleId, s.code);
151
+ else cost.mayHaveSpent(s.roleId, s.code);
152
+ }
153
+ ```
154
+
155
+ `skipSpentNoModelCall` answers false for `turn_error` even though it often is
156
+ free: context assembly failing and a provider dying mid-stream both land there
157
+ and cannot be told apart afterwards, so a cost model that counted it free would
158
+ under-count silently. It answers false for an unknown code for the same reason.
159
+
160
+ To read a beat, subtract: a role in `selectedRoles` and absent from
161
+ `skippedRoles` chose silence; a role listed here never ran.
162
+
138
163
  **Resuming after a crash.** `getTurn` returns the same fields the live result
139
164
  did — `nextOptions` included — so a recovered session can offer the audience
140
165
  the choices it was about to. To catch up on beats you missed entirely, store
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * a project is running — which is exactly the field you reach for when a
5
5
  * customer's integration behaves like an older SDK than they say they have.
6
6
  * It sat at '0.1.0' for eight releases before anything compared the two. */
7
- export declare const WORLD_SDK_VERSION = "0.28.1";
7
+ export declare const WORLD_SDK_VERSION = "0.30.0";
8
8
  export declare const DEFAULT_BASE_URL = "https://pouchy.ai/v1";
9
9
  /** One direction a beat could take. The WHOLE of what a deliberation shows a
10
10
  * player: no reasoning, no role secrets, no simulated effects, no scores. */
@@ -176,6 +176,7 @@ export interface WorldTurnResult {
176
176
  skippedRoles: Array<{
177
177
  roleId: string;
178
178
  reason: string;
179
+ code?: WorldSkipCode;
179
180
  }>;
180
181
  nextOptions: Array<{
181
182
  branchId: string;
@@ -381,6 +382,7 @@ export interface WorldTurnReadback {
381
382
  skippedRoles?: Array<{
382
383
  roleId: string;
383
384
  reason: string;
385
+ code?: WorldSkipCode;
384
386
  }>;
385
387
  repairs?: Array<{
386
388
  roleId: string;
@@ -525,6 +527,44 @@ export interface EditorialDraftRow {
525
527
  * content problem. */
526
528
  /** The shared refusal taxonomy (Batch 7). Only `narrative_conflict` is content. */
527
529
  export type WorldRejectionCode = 'narrative_conflict' | 'policy_rejection' | 'schema_rejection' | 'canon_rejection' | 'concurrency_rejection' | 'repair_failure';
530
+ /** WHY a role that was in the cast did not speak — the machine half of
531
+ * `skippedRoles[].reason`, added 2026-09-09.
532
+ *
533
+ * `reason` remains the human line and carries the sub-cause (which admission
534
+ * rule, which runtime error); it is free to be reworded, so do not key off it.
535
+ * This exists because an integrator built a cost model on
536
+ * `reason.startsWith('turn error:')` — a rename on the platform side would not
537
+ * have errored, it would have silently reclassified that whole class.
538
+ *
539
+ * No model call was made for the first eight; a model ran, or may have, for
540
+ * `turn_error` and `no_message`; the last two are beats where the role DID
541
+ * speak and the line was withheld. `skipSpentNoModelCall` below is that split,
542
+ * so a consumer does not re-derive it. Absent on turns from a server predating
543
+ * the taxonomy — read that as unknown, never as a value. */
544
+ export type WorldSkipCode = 'not_focused' | 'role_cap_reached' | 'no_actor_bound' | 'deadline_exhausted' | 'not_admitted' | 'actor_unavailable' | 'session_busy' | 'content_policy' | 'turn_error' | 'no_message' | 'effect_refused' | 'conflict_undelivered';
545
+ /** The cost model's version and digest AS OF THIS SDK BUILD.
546
+ *
547
+ * `skipSpentNoModelCall` below is a SNAPSHOT — a published package cannot
548
+ * track a server that regroups a code later, and nothing would error if it
549
+ * did: your code would not change, your tests would stay green, and your
550
+ * billing would drift. So compare these against `x-pouchy-cost-model` in
551
+ * `GET /v1/world/openapi` (the same document you already read for the enum) and
552
+ * alarm on a mismatch. That check is worth more than any promise about version
553
+ * bumps, because it fires on the condition itself rather than on our
554
+ * remembering to signal it.
555
+ *
556
+ * The digest covers group MEMBERSHIP only: reordering the enum or rewording a
557
+ * comment never moves it. */
558
+ export declare const SKIP_COST_MODEL_VERSION = "1.0.0";
559
+ export declare const SKIP_COST_MODEL_DIGEST = "4a8369322817e3fa";
560
+ /** Did this skip happen before any model call — is the role's silence free?
561
+ *
562
+ * `turn_error` is deliberately NOT in the free set even though it often is:
563
+ * context assembly failing and a provider dying mid-stream both land there and
564
+ * cannot be told apart after the fact, so a cost model that counted it free
565
+ * would under-count silently. Returns false for an unknown code, for the same
566
+ * reason. */
567
+ export declare function skipSpentNoModelCall(code: WorldSkipCode | undefined): boolean;
528
568
  /** True for every code except `narrative_conflict`. */
529
569
  export declare function isDefectRejection(code: WorldRejectionCode | undefined): boolean;
530
570
  export interface WorldInstanceMetrics {
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ import { createHash, createHmac, randomUUID } from 'node:crypto';
23
23
  * a project is running — which is exactly the field you reach for when a
24
24
  * customer's integration behaves like an older SDK than they say they have.
25
25
  * It sat at '0.1.0' for eight releases before anything compared the two. */
26
- export const WORLD_SDK_VERSION = '0.28.1';
26
+ export const WORLD_SDK_VERSION = '0.30.0';
27
27
  export const DEFAULT_BASE_URL = 'https://pouchy.ai/v1';
28
28
  /** Read the commit turn id out of an envelope, so the signature covers the id
29
29
  * the server will commit under. The payload half is base64url JSON; the
@@ -306,6 +306,38 @@ export function describeTurn(result) {
306
306
  ` delivery ${result.deliveryStatus}`
307
307
  };
308
308
  }
309
+ /** The cost model's version and digest AS OF THIS SDK BUILD.
310
+ *
311
+ * `skipSpentNoModelCall` below is a SNAPSHOT — a published package cannot
312
+ * track a server that regroups a code later, and nothing would error if it
313
+ * did: your code would not change, your tests would stay green, and your
314
+ * billing would drift. So compare these against `x-pouchy-cost-model` in
315
+ * `GET /v1/world/openapi` (the same document you already read for the enum) and
316
+ * alarm on a mismatch. That check is worth more than any promise about version
317
+ * bumps, because it fires on the condition itself rather than on our
318
+ * remembering to signal it.
319
+ *
320
+ * The digest covers group MEMBERSHIP only: reordering the enum or rewording a
321
+ * comment never moves it. */
322
+ export const SKIP_COST_MODEL_VERSION = '1.0.0';
323
+ export const SKIP_COST_MODEL_DIGEST = '4a8369322817e3fa';
324
+ /** Did this skip happen before any model call — is the role's silence free?
325
+ *
326
+ * `turn_error` is deliberately NOT in the free set even though it often is:
327
+ * context assembly failing and a provider dying mid-stream both land there and
328
+ * cannot be told apart after the fact, so a cost model that counted it free
329
+ * would under-count silently. Returns false for an unknown code, for the same
330
+ * reason. */
331
+ export function skipSpentNoModelCall(code) {
332
+ return (code === 'not_focused' ||
333
+ code === 'role_cap_reached' ||
334
+ code === 'no_actor_bound' ||
335
+ code === 'deadline_exhausted' ||
336
+ code === 'not_admitted' ||
337
+ code === 'actor_unavailable' ||
338
+ code === 'session_busy' ||
339
+ code === 'content_policy');
340
+ }
309
341
  /** True for every code except `narrative_conflict`. */
310
342
  export function isDefectRejection(code) {
311
343
  return code !== undefined && code !== 'narrative_conflict';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/world-sdk",
3
- "version": "0.28.1",
3
+ "version": "0.30.0",
4
4
  "description": "Server-side TypeScript client for Pouchy World \u2014 story packages, world definitions, world sessions, coordinated turns, trusted events, replay verification and script drafts. Node only: it holds a project Secret Key and a source signing key, which never belong in a browser or a mobile app.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",