@pouchy_ai/world-sdk 0.28.0 → 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,43 @@
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
+
30
+ ## 0.28.1
31
+
32
+ - **`WorldApiError.errorId`** — the server's lookup reference (`err_…`) for a
33
+ persisted 5xx, when the envelope carried one. An uncaught world-plane fault is
34
+ sanitized to "An unexpected error occurred." before it leaves the server; the
35
+ `errorId` on that envelope is the one thing an operator can resolve to the
36
+ real cause, and the client read `error` / `code` / `retryAfterSec` and dropped
37
+ it — so a 5xx gave you `500 server_error` and nothing to quote. Additive;
38
+ absent on every 4xx and on a 5xx that was not persisted; string-only, like
39
+ `serverCode`. Quote it in a support request.
40
+
3
41
  ## 0.28.0
4
42
 
5
43
  - **`WorldApiError.serverCode`** — the server's own machine-readable `code`
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
@@ -251,7 +276,8 @@ an `approved` editorial can be exported.
251
276
  `newTurnId` / `isReservedTurnId` (idempotency), `describeTurn` (read a result
252
277
  without guessing), `WorldApiError` with typed codes, `.retryable`,
253
278
  `serverCode` (the server's own `code` on a refusal — which 409, since `code`
254
- maps the status), and `rejectedEffects` (which of YOUR proposed ops the world
279
+ maps the status), `errorId` (the server's `err_…` lookup reference on a
280
+ persisted 5xx — quote it in a support request), and `rejectedEffects` (which of YOUR proposed ops the world
255
281
  refused, and why — validation is all-or-nothing, so one bad op moves nothing).
256
282
 
257
283
  ## When a signed door refuses you
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.0";
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. */
@@ -61,6 +61,13 @@ export declare class WorldApiError extends Error {
61
61
  * dropped `code` on the floor, so the one carrier an integrator could
62
62
  * branch on never arrived. */
63
63
  readonly serverCode?: string;
64
+ /** The server's lookup reference for a persisted 5xx (`err_…`), when the
65
+ * envelope carried one. A world-plane fault is sanitized to "An unexpected
66
+ * error occurred." before it leaves the server; the `errorId` is the ONE
67
+ * greppable thing the operator can resolve to the real cause — quote it in
68
+ * a support request. Absent on every 4xx and on a 5xx that was not
69
+ * persisted (0.28.1; until then the client dropped it). */
70
+ readonly errorId?: string;
64
71
  readonly retryAfterSec?: number;
65
72
  /** Why the world refused the effects you proposed — per op, with the
66
73
  * server's own reason and code.
@@ -90,6 +97,7 @@ export declare class WorldApiError extends Error {
90
97
  message: string;
91
98
  detail?: string;
92
99
  serverCode?: string;
100
+ errorId?: string;
93
101
  retryAfterSec?: number;
94
102
  rejectedEffects?: readonly {
95
103
  readonly index: number;
@@ -168,6 +176,7 @@ export interface WorldTurnResult {
168
176
  skippedRoles: Array<{
169
177
  roleId: string;
170
178
  reason: string;
179
+ code?: WorldSkipCode;
171
180
  }>;
172
181
  nextOptions: Array<{
173
182
  branchId: string;
@@ -373,6 +382,7 @@ export interface WorldTurnReadback {
373
382
  skippedRoles?: Array<{
374
383
  roleId: string;
375
384
  reason: string;
385
+ code?: WorldSkipCode;
376
386
  }>;
377
387
  repairs?: Array<{
378
388
  roleId: string;
@@ -517,6 +527,29 @@ export interface EditorialDraftRow {
517
527
  * content problem. */
518
528
  /** The shared refusal taxonomy (Batch 7). Only `narrative_conflict` is content. */
519
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;
520
553
  /** True for every code except `narrative_conflict`. */
521
554
  export declare function isDefectRejection(code: WorldRejectionCode | undefined): boolean;
522
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.0';
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
@@ -71,6 +71,13 @@ export class WorldApiError extends Error {
71
71
  * dropped `code` on the floor, so the one carrier an integrator could
72
72
  * branch on never arrived. */
73
73
  serverCode;
74
+ /** The server's lookup reference for a persisted 5xx (`err_…`), when the
75
+ * envelope carried one. A world-plane fault is sanitized to "An unexpected
76
+ * error occurred." before it leaves the server; the `errorId` is the ONE
77
+ * greppable thing the operator can resolve to the real cause — quote it in
78
+ * a support request. Absent on every 4xx and on a 5xx that was not
79
+ * persisted (0.28.1; until then the client dropped it). */
80
+ errorId;
74
81
  retryAfterSec;
75
82
  /** Why the world refused the effects you proposed — per op, with the
76
83
  * server's own reason and code.
@@ -97,6 +104,8 @@ export class WorldApiError extends Error {
97
104
  this.detail = input.detail;
98
105
  if (input.serverCode !== undefined)
99
106
  this.serverCode = input.serverCode;
107
+ if (input.errorId !== undefined)
108
+ this.errorId = input.errorId;
100
109
  if (input.retryAfterSec !== undefined)
101
110
  this.retryAfterSec = input.retryAfterSec;
102
111
  if (input.rejectedEffects !== undefined)
@@ -297,6 +306,23 @@ export function describeTurn(result) {
297
306
  ` delivery ${result.deliveryStatus}`
298
307
  };
299
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
+ }
300
326
  /** True for every code except `narrative_conflict`. */
301
327
  export function isDefectRejection(code) {
302
328
  return code !== undefined && code !== 'narrative_conflict';
@@ -940,12 +966,18 @@ export class PouchyWorldClient {
940
966
  const serverCode = parsed && typeof parsed === 'object' && typeof parsed.code === 'string'
941
967
  ? parsed.code
942
968
  : undefined;
969
+ // Same string-only rule as `serverCode`: the envelope's lookup ref, or
970
+ // nothing — never a coerced non-string.
971
+ const errorId = parsed && typeof parsed === 'object' && typeof parsed.errorId === 'string'
972
+ ? parsed.errorId || undefined
973
+ : undefined;
943
974
  throw new WorldApiError({
944
975
  code: codeForStatus(response.status),
945
976
  status: response.status,
946
977
  message: `${method} ${path} → ${response.status}`,
947
978
  ...(detail !== undefined ? { detail } : {}),
948
979
  ...(serverCode !== undefined ? { serverCode } : {}),
980
+ ...(errorId !== undefined ? { errorId } : {}),
949
981
  ...(retryAfterSec !== undefined ? { retryAfterSec } : {}),
950
982
  ...(rejectedEffects !== undefined ? { rejectedEffects } : {})
951
983
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/world-sdk",
3
- "version": "0.28.0",
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",