@pouchy_ai/world-sdk 0.28.1 → 0.29.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,32 @@
1
1
  # @pouchy_ai/world-sdk
2
2
 
3
+ ## 0.29.0
4
+
5
+ - **`skippedRoles[].code`** — the machine half of "why this role did not
6
+ speak", as a closed `WorldSkipCode` union, plus `skipSpentNoModelCall(code)`
7
+ for the one derived question every consumer asks of it.
8
+
9
+ `reason` has always been a human sentence and stays one; nothing should key
10
+ off it. This exists because an integrator, given the reason prefixes in a
11
+ letter (the spec published none), built a cost model on
12
+ `reason.startsWith('turn error:')`. Their own words for the exposure: rename
13
+ that prefix and "we would not error — we would quietly file that class as
14
+ costing nothing." The same argument the rejection taxonomy made for
15
+ `rejectedEffects[].code`, applied to the sibling array that missed it.
16
+
17
+ Twelve codes. Eight of them mean no model call was made — `not_focused`,
18
+ `role_cap_reached`, `no_actor_bound`, `deadline_exhausted`, `not_admitted`,
19
+ `actor_unavailable`, `session_busy`, `content_policy`. Two mean a model ran or
20
+ may have: `turn_error` (deliberately does NOT promise a call happened —
21
+ context assembly failing and a provider dying mid-stream both land there) and
22
+ `no_message`. Two are beats where the role DID speak and the line was
23
+ withheld: `effect_refused`, `conflict_undelivered`.
24
+
25
+ Additive and optional: absent from a server predating the taxonomy, which
26
+ reads as unknown, never as a value. `skipSpentNoModelCall` returns false for
27
+ an unknown code and for `turn_error`, on the principle that a cost model
28
+ which under-counts silently is worse than one that over-counts visibly.
29
+
3
30
  ## 0.28.1
4
31
 
5
32
  - **`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.29.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,29 @@ 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
+ /** Did this skip happen before any model call — is the role's silence free?
546
+ *
547
+ * `turn_error` is deliberately NOT in the free set even though it often is:
548
+ * context assembly failing and a provider dying mid-stream both land there and
549
+ * cannot be told apart after the fact, so a cost model that counted it free
550
+ * would under-count silently. Returns false for an unknown code, for the same
551
+ * reason. */
552
+ export declare function skipSpentNoModelCall(code: WorldSkipCode | undefined): boolean;
528
553
  /** True for every code except `narrative_conflict`. */
529
554
  export declare function isDefectRejection(code: WorldRejectionCode | undefined): boolean;
530
555
  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.29.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,23 @@ export function describeTurn(result) {
306
306
  ` delivery ${result.deliveryStatus}`
307
307
  };
308
308
  }
309
+ /** Did this skip happen before any model call — is the role's silence free?
310
+ *
311
+ * `turn_error` is deliberately NOT in the free set even though it often is:
312
+ * context assembly failing and a provider dying mid-stream both land there and
313
+ * cannot be told apart after the fact, so a cost model that counted it free
314
+ * would under-count silently. Returns false for an unknown code, for the same
315
+ * reason. */
316
+ export function skipSpentNoModelCall(code) {
317
+ return (code === 'not_focused' ||
318
+ code === 'role_cap_reached' ||
319
+ code === 'no_actor_bound' ||
320
+ code === 'deadline_exhausted' ||
321
+ code === 'not_admitted' ||
322
+ code === 'actor_unavailable' ||
323
+ code === 'session_busy' ||
324
+ code === 'content_policy');
325
+ }
309
326
  /** True for every code except `narrative_conflict`. */
310
327
  export function isDefectRejection(code) {
311
328
  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.29.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",