@pouchy_ai/world-sdk 0.24.0 → 0.25.1
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 +32 -0
- package/README.md +10 -6
- package/dist/index.d.ts +30 -1
- package/dist/index.js +54 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# @pouchy_ai/world-sdk
|
|
2
2
|
|
|
3
|
+
## 0.25.1
|
|
4
|
+
|
|
5
|
+
- **Docs links now resolve.** The README sent readers to `docs/*.md` "in the Pouchy
|
|
6
|
+
repo" — which is PRIVATE, so every pointer in the shipped package was a dead end
|
|
7
|
+
for the people the package is for. The world documentation is now published at
|
|
8
|
+
pouchy.ai/docs, and the README links there.
|
|
9
|
+
- No code change; the client surface is byte-identical to 0.25.0.
|
|
10
|
+
|
|
11
|
+
## 0.25.0
|
|
12
|
+
|
|
13
|
+
- **`WorldApiError.rejectedEffects`** — a rejected turn (422) now arrives with
|
|
14
|
+
the per-op reasons the server computed, instead of a bare status. The turns
|
|
15
|
+
door answers a refused effect batch with the FULL turn result rather than
|
|
16
|
+
`{ error }`, so `detail` was undefined for exactly the refusal that has the
|
|
17
|
+
most to say, and an integrator got `POST … → 422` and nothing else.
|
|
18
|
+
- This is load-bearing rather than cosmetic because patch validation is
|
|
19
|
+
ALL-OR-NOTHING: one bad op voids the batch, the turn moves nothing, and
|
|
20
|
+
"which of my ops was it" is the only useful question. Measured cost of not
|
|
21
|
+
having it: two rounds of field testing spent guessing which of five ops was
|
|
22
|
+
bad while the server had already written the answer (`node "node.hear-amara"
|
|
23
|
+
requires "node.warning" to be completed first`).
|
|
24
|
+
- Each entry carries `index`, `reason`, and optionally `code` / `roleId` /
|
|
25
|
+
`kind`. Read defensively from an untrusted body: entries without a string
|
|
26
|
+
`reason` are dropped, and the field is ABSENT (never `[]`) when the server
|
|
27
|
+
named nothing — "said nothing" and "refused nothing" stay distinguishable.
|
|
28
|
+
- Purely additive; `code`, `status`, `detail`, `retryAfterSec` and `.retryable`
|
|
29
|
+
are unchanged, and knowing why a batch was refused does not make a 422
|
|
30
|
+
retryable.
|
|
31
|
+
- See `docs/world-sdk-errors.md` → “`rejectedEffects`” for the two readings
|
|
32
|
+
that matter (an undeclared id means a stale pinned revision; an unmet
|
|
33
|
+
prerequisite means an earlier beat never committed).
|
|
34
|
+
|
|
3
35
|
## 0.24.0
|
|
4
36
|
|
|
5
37
|
- **`startNextEpisode` now rides the MACHINE lane** (Secret Key + provider
|
package/README.md
CHANGED
|
@@ -240,7 +240,9 @@ an `approved` editorial can be exported.
|
|
|
240
240
|
|
|
241
241
|
**Helpers** — `signSourceRequest` (the exact canonical the server verifies),
|
|
242
242
|
`newTurnId` / `isReservedTurnId` (idempotency), `describeTurn` (read a result
|
|
243
|
-
without guessing), `WorldApiError` with typed codes
|
|
243
|
+
without guessing), `WorldApiError` with typed codes, `.retryable`, and
|
|
244
|
+
`rejectedEffects` (which of YOUR proposed ops the world refused, and why —
|
|
245
|
+
validation is all-or-nothing, so one bad op moves nothing).
|
|
244
246
|
|
|
245
247
|
## When a signed door refuses you
|
|
246
248
|
|
|
@@ -264,8 +266,8 @@ It returns the recent refusals with a closed reason vocabulary — `missing`,
|
|
|
264
266
|
`malformed`, `unknown_key`, `stale`, `bad_signature`, `no_keys` — plus how many
|
|
265
267
|
audit rows it scanned, because an empty feed is not a clean bill of health.
|
|
266
268
|
|
|
267
|
-
|
|
268
|
-
`WorldApiError` code to what to change.
|
|
269
|
+
[World SDK — errors and refusals](https://pouchy.ai/docs/world-sdk-errors) maps
|
|
270
|
+
every reason and every `WorldApiError` code to what to change.
|
|
269
271
|
|
|
270
272
|
## The three things integrators get wrong
|
|
271
273
|
|
|
@@ -308,11 +310,13 @@ and exits non-zero on the first structural failure.
|
|
|
308
310
|
|
|
309
311
|
## Quickstarts
|
|
310
312
|
|
|
311
|
-
-
|
|
312
|
-
-
|
|
313
|
-
-
|
|
313
|
+
- [An interactive drama](https://pouchy.ai/docs/world-quickstart-drama) — screenplay → world → new screenplay draft
|
|
314
|
+
- [NPCs that share one world](https://pouchy.ai/docs/world-quickstart-npc) — one town, three NPCs, one shared state
|
|
315
|
+
- [Inside your Next.js app](https://pouchy.ai/docs/world-quickstart-nextjs) — one route handler, one page,
|
|
314
316
|
no browser login anywhere in the deployment
|
|
315
317
|
|
|
318
|
+
Everything else starts at the [documentation index](https://pouchy.ai/docs/pouchy-world).
|
|
319
|
+
|
|
316
320
|
## License
|
|
317
321
|
|
|
318
322
|
See LICENSE.
|
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.
|
|
7
|
+
export declare const WORLD_SDK_VERSION = "0.25.1";
|
|
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. */
|
|
@@ -41,12 +41,41 @@ export declare class WorldApiError extends Error {
|
|
|
41
41
|
/** The server's own message, when it sent one. */
|
|
42
42
|
readonly detail?: string;
|
|
43
43
|
readonly retryAfterSec?: number;
|
|
44
|
+
/** Why the world refused the effects you proposed — per op, with the
|
|
45
|
+
* server's own reason and code.
|
|
46
|
+
*
|
|
47
|
+
* A rejected turn (422) does NOT answer with `{ error }` like every other
|
|
48
|
+
* refusal: its body is the full turn result, and the reasons live in
|
|
49
|
+
* `rejectedEffects`. So `detail` was undefined for exactly the refusal
|
|
50
|
+
* that has the most to say, and an integrator got `POST … → 422` and
|
|
51
|
+
* nothing else. Measured cost: two rounds of field testing spent guessing
|
|
52
|
+
* which of five ops in a batch was the bad one, while the server had
|
|
53
|
+
* computed the answer (`node "node.hear-amara" requires "node.warning" to
|
|
54
|
+
* be completed first`) and thrown it away at this line.
|
|
55
|
+
*
|
|
56
|
+
* Patch validation is ALL-OR-NOTHING, which is what makes this
|
|
57
|
+
* load-bearing rather than nice-to-have: one bad op voids the batch, so
|
|
58
|
+
* "which one" is the only question worth asking. */
|
|
59
|
+
readonly rejectedEffects?: readonly {
|
|
60
|
+
readonly index: number;
|
|
61
|
+
readonly reason: string;
|
|
62
|
+
readonly roleId?: string;
|
|
63
|
+
readonly kind?: string;
|
|
64
|
+
readonly code?: string;
|
|
65
|
+
}[];
|
|
44
66
|
constructor(input: {
|
|
45
67
|
code: WorldErrorCode;
|
|
46
68
|
status: number;
|
|
47
69
|
message: string;
|
|
48
70
|
detail?: string;
|
|
49
71
|
retryAfterSec?: number;
|
|
72
|
+
rejectedEffects?: readonly {
|
|
73
|
+
readonly index: number;
|
|
74
|
+
readonly reason: string;
|
|
75
|
+
readonly roleId?: string;
|
|
76
|
+
readonly kind?: string;
|
|
77
|
+
readonly code?: string;
|
|
78
|
+
}[];
|
|
50
79
|
});
|
|
51
80
|
/** Worth trying again with the SAME idempotency key. A 409 is not: it means
|
|
52
81
|
* the world disagreed with the request, and repeating it will disagree
|
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.
|
|
26
|
+
export const WORLD_SDK_VERSION = '0.25.1';
|
|
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
|
|
@@ -61,6 +61,22 @@ export class WorldApiError extends Error {
|
|
|
61
61
|
/** The server's own message, when it sent one. */
|
|
62
62
|
detail;
|
|
63
63
|
retryAfterSec;
|
|
64
|
+
/** Why the world refused the effects you proposed — per op, with the
|
|
65
|
+
* server's own reason and code.
|
|
66
|
+
*
|
|
67
|
+
* A rejected turn (422) does NOT answer with `{ error }` like every other
|
|
68
|
+
* refusal: its body is the full turn result, and the reasons live in
|
|
69
|
+
* `rejectedEffects`. So `detail` was undefined for exactly the refusal
|
|
70
|
+
* that has the most to say, and an integrator got `POST … → 422` and
|
|
71
|
+
* nothing else. Measured cost: two rounds of field testing spent guessing
|
|
72
|
+
* which of five ops in a batch was the bad one, while the server had
|
|
73
|
+
* computed the answer (`node "node.hear-amara" requires "node.warning" to
|
|
74
|
+
* be completed first`) and thrown it away at this line.
|
|
75
|
+
*
|
|
76
|
+
* Patch validation is ALL-OR-NOTHING, which is what makes this
|
|
77
|
+
* load-bearing rather than nice-to-have: one bad op voids the batch, so
|
|
78
|
+
* "which one" is the only question worth asking. */
|
|
79
|
+
rejectedEffects;
|
|
64
80
|
constructor(input) {
|
|
65
81
|
super(input.message);
|
|
66
82
|
this.name = 'WorldApiError';
|
|
@@ -70,6 +86,8 @@ export class WorldApiError extends Error {
|
|
|
70
86
|
this.detail = input.detail;
|
|
71
87
|
if (input.retryAfterSec !== undefined)
|
|
72
88
|
this.retryAfterSec = input.retryAfterSec;
|
|
89
|
+
if (input.rejectedEffects !== undefined)
|
|
90
|
+
this.rejectedEffects = input.rejectedEffects;
|
|
73
91
|
}
|
|
74
92
|
/** Worth trying again with the SAME idempotency key. A 409 is not: it means
|
|
75
93
|
* the world disagreed with the request, and repeating it will disagree
|
|
@@ -84,6 +102,38 @@ export class WorldApiError extends Error {
|
|
|
84
102
|
* delta-seconds number, then the HTTP-date form proxies and CDNs emit.
|
|
85
103
|
* Anything that is not a non-negative finite number is `undefined` — no
|
|
86
104
|
* guessing. */
|
|
105
|
+
/** The per-op refusal reasons a rejected turn carries, defensively read.
|
|
106
|
+
*
|
|
107
|
+
* Shaped from an untrusted body, so every field is checked rather than cast:
|
|
108
|
+
* a client that promises `rejectedEffects[].reason` is a string and hands the
|
|
109
|
+
* caller `undefined` has moved the failure one frame later, into the
|
|
110
|
+
* integrator's own logging. Entries that are not a plausible refusal are
|
|
111
|
+
* dropped, and an empty result is `undefined` — absent, not `[]`, so "the
|
|
112
|
+
* server said nothing" and "the server refused nothing" stay different
|
|
113
|
+
* readings. */
|
|
114
|
+
function rejectedEffectsFrom(body) {
|
|
115
|
+
if (!body || typeof body !== 'object')
|
|
116
|
+
return undefined;
|
|
117
|
+
const raw = body.rejectedEffects;
|
|
118
|
+
if (!Array.isArray(raw))
|
|
119
|
+
return undefined;
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const e of raw.slice(0, 64)) {
|
|
122
|
+
if (!e || typeof e !== 'object')
|
|
123
|
+
continue;
|
|
124
|
+
const r = e;
|
|
125
|
+
if (typeof r.reason !== 'string' || !r.reason)
|
|
126
|
+
continue;
|
|
127
|
+
out.push({
|
|
128
|
+
index: typeof r.index === 'number' ? r.index : -1,
|
|
129
|
+
reason: r.reason.slice(0, 500),
|
|
130
|
+
...(typeof r.roleId === 'string' ? { roleId: r.roleId } : {}),
|
|
131
|
+
...(typeof r.kind === 'string' ? { kind: r.kind } : {}),
|
|
132
|
+
...(typeof r.code === 'string' ? { code: r.code } : {})
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return out.length ? out : undefined;
|
|
136
|
+
}
|
|
87
137
|
function retryAfterFrom(res, body) {
|
|
88
138
|
for (const k of ['retryAfterSec', 'retryAfter']) {
|
|
89
139
|
const v = body?.[k];
|
|
@@ -870,12 +920,14 @@ export class PouchyWorldClient {
|
|
|
870
920
|
? String(parsed.error)
|
|
871
921
|
: undefined;
|
|
872
922
|
const retryAfterSec = retryAfterFrom(response, parsed && typeof parsed === 'object' ? parsed : null);
|
|
923
|
+
const rejectedEffects = rejectedEffectsFrom(parsed);
|
|
873
924
|
throw new WorldApiError({
|
|
874
925
|
code: codeForStatus(response.status),
|
|
875
926
|
status: response.status,
|
|
876
927
|
message: `${method} ${path} → ${response.status}`,
|
|
877
928
|
...(detail !== undefined ? { detail } : {}),
|
|
878
|
-
...(retryAfterSec !== undefined ? { retryAfterSec } : {})
|
|
929
|
+
...(retryAfterSec !== undefined ? { retryAfterSec } : {}),
|
|
930
|
+
...(rejectedEffects !== undefined ? { rejectedEffects } : {})
|
|
879
931
|
});
|
|
880
932
|
}
|
|
881
933
|
return parsed;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pouchy_ai/world-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.1",
|
|
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",
|