@pouchy_ai/world-sdk 0.23.0 → 0.25.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 +42 -0
- package/README.md +7 -2
- package/dist/index.d.ts +41 -5
- package/dist/index.js +68 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# @pouchy_ai/world-sdk
|
|
2
2
|
|
|
3
|
+
## 0.25.0
|
|
4
|
+
|
|
5
|
+
- **`WorldApiError.rejectedEffects`** — a rejected turn (422) now arrives with
|
|
6
|
+
the per-op reasons the server computed, instead of a bare status. The turns
|
|
7
|
+
door answers a refused effect batch with the FULL turn result rather than
|
|
8
|
+
`{ error }`, so `detail` was undefined for exactly the refusal that has the
|
|
9
|
+
most to say, and an integrator got `POST … → 422` and nothing else.
|
|
10
|
+
- This is load-bearing rather than cosmetic because patch validation is
|
|
11
|
+
ALL-OR-NOTHING: one bad op voids the batch, the turn moves nothing, and
|
|
12
|
+
"which of my ops was it" is the only useful question. Measured cost of not
|
|
13
|
+
having it: two rounds of field testing spent guessing which of five ops was
|
|
14
|
+
bad while the server had already written the answer (`node "node.hear-amara"
|
|
15
|
+
requires "node.warning" to be completed first`).
|
|
16
|
+
- Each entry carries `index`, `reason`, and optionally `code` / `roleId` /
|
|
17
|
+
`kind`. Read defensively from an untrusted body: entries without a string
|
|
18
|
+
`reason` are dropped, and the field is ABSENT (never `[]`) when the server
|
|
19
|
+
named nothing — "said nothing" and "refused nothing" stay distinguishable.
|
|
20
|
+
- Purely additive; `code`, `status`, `detail`, `retryAfterSec` and `.retryable`
|
|
21
|
+
are unchanged, and knowing why a batch was refused does not make a 422
|
|
22
|
+
retryable.
|
|
23
|
+
- See `docs/world-sdk-errors.md` → “`rejectedEffects`” for the two readings
|
|
24
|
+
that matter (an undeclared id means a stale pinned revision; an unmet
|
|
25
|
+
prerequisite means an earlier beat never committed).
|
|
26
|
+
|
|
27
|
+
## 0.24.0
|
|
28
|
+
|
|
29
|
+
- **`startNextEpisode` now rides the MACHINE lane** (Secret Key + provider
|
|
30
|
+
signature, id namespace `epn:`) — the same two proofs as `runTurn` — so an
|
|
31
|
+
unattended serial backend can advance its own player's worldline. Before
|
|
32
|
+
this it required the expiring owner token, which made serial UX impossible
|
|
33
|
+
for an integrator: the backend that drives every beat of the story could
|
|
34
|
+
not move it to the next episode (#2998).
|
|
35
|
+
- The method signature is unchanged (`startNextEpisode(environmentId,
|
|
36
|
+
worldInstanceId)`); an optional `{ requestId }` third argument pins the
|
|
37
|
+
signature id slot for callers that want to supply their own.
|
|
38
|
+
- The server still accepts the owner lane, and still exposes no `/admin`
|
|
39
|
+
mirror — an admin key proves the project, never the Provider.
|
|
40
|
+
- Idempotence is untouched: the run id stays server-derived from the
|
|
41
|
+
worldline, the episode and the revision it starts at; `requestId` is a
|
|
42
|
+
signature slot, not an idempotency key.
|
|
43
|
+
- Server-side this is world API 1.30.0.
|
|
44
|
+
|
|
3
45
|
## 0.23.0
|
|
4
46
|
|
|
5
47
|
- **`runTurn` gains `focusRoles?: string[]`** — narrow one beat to the named
|
package/README.md
CHANGED
|
@@ -45,7 +45,10 @@ const world = new PouchyWorldClient({
|
|
|
45
45
|
**Authoring** — `createStoryPackage`, `publishStoryPackage`, `getStoryPackage`,
|
|
46
46
|
`listStoryPackages`, `createWorld`, `publishWorld`, `getWorld`, `listWorlds`.
|
|
47
47
|
|
|
48
|
-
**Running** — `createWorldSession`, `runTurn`, `sendEvent
|
|
48
|
+
**Running** — `createWorldSession`, `runTurn`, `sendEvent`,
|
|
49
|
+
`startNextEpisode` (machine lane since 0.24.0 — an unattended serial backend
|
|
50
|
+
can advance its own player's worldline to the next episode and receive the
|
|
51
|
+
carryover).
|
|
49
52
|
|
|
50
53
|
**Deliberating** — `deliberate`, `selectCandidate`. Ask for a couple of public
|
|
51
54
|
directions a beat could take, then commit the one the player picked. Off unless
|
|
@@ -237,7 +240,9 @@ an `approved` editorial can be exported.
|
|
|
237
240
|
|
|
238
241
|
**Helpers** — `signSourceRequest` (the exact canonical the server verifies),
|
|
239
242
|
`newTurnId` / `isReservedTurnId` (idempotency), `describeTurn` (read a result
|
|
240
|
-
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).
|
|
241
246
|
|
|
242
247
|
## When a signed door refuses you
|
|
243
248
|
|
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.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. */
|
|
@@ -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
|
|
@@ -1032,10 +1061,17 @@ export declare class PouchyWorldClient {
|
|
|
1032
1061
|
* revision it starts at, so a retried call lands on the same run and
|
|
1033
1062
|
* answers `created: false` rather than forking the story.
|
|
1034
1063
|
*
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1037
|
-
*
|
|
1038
|
-
|
|
1064
|
+
* MACHINE LANE since 0.24.0 (#2998, world API 1.30.0): a Secret Key plus a
|
|
1065
|
+
* provider signature in the `epn:` namespace — the same two proofs as
|
|
1066
|
+
* `runTurn`, because the party trusted to author every beat, including the
|
|
1067
|
+
* one that ends an episode, is the party deciding the story moves on. So
|
|
1068
|
+
* an unattended serial backend can now advance its own player's worldline.
|
|
1069
|
+
* (Before 0.24.0 this rode the owner token; the server still accepts that
|
|
1070
|
+
* lane, and still exposes no `/admin` mirror — an admin key proves the
|
|
1071
|
+
* project, never the Provider.) */
|
|
1072
|
+
startNextEpisode(environmentId: string, worldInstanceId: string, opts?: {
|
|
1073
|
+
requestId?: string;
|
|
1074
|
+
}): Promise<WorldNextEpisodeResponse>;
|
|
1039
1075
|
/** Publish the next world revision. Existing world INSTANCES keep the
|
|
1040
1076
|
* revision they were created on — a published change reaches new instances
|
|
1041
1077
|
* only, which is what keeps a running story from changing runtime or rules
|
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.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
|
|
@@ -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];
|
|
@@ -356,11 +406,20 @@ export class PouchyWorldClient {
|
|
|
356
406
|
* revision it starts at, so a retried call lands on the same run and
|
|
357
407
|
* answers `created: false` rather than forking the story.
|
|
358
408
|
*
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
|
|
363
|
-
|
|
409
|
+
* MACHINE LANE since 0.24.0 (#2998, world API 1.30.0): a Secret Key plus a
|
|
410
|
+
* provider signature in the `epn:` namespace — the same two proofs as
|
|
411
|
+
* `runTurn`, because the party trusted to author every beat, including the
|
|
412
|
+
* one that ends an episode, is the party deciding the story moves on. So
|
|
413
|
+
* an unattended serial backend can now advance its own player's worldline.
|
|
414
|
+
* (Before 0.24.0 this rode the owner token; the server still accepts that
|
|
415
|
+
* lane, and still exposes no `/admin` mirror — an admin key proves the
|
|
416
|
+
* project, never the Provider.) */
|
|
417
|
+
startNextEpisode(environmentId, worldInstanceId, opts = {}) {
|
|
418
|
+
const requestId = opts.requestId ?? newTurnId('epn-req');
|
|
419
|
+
return this.signed(`/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/episodes/next`, { requestId },
|
|
420
|
+
// Namespaced id slot: an episode-start signature can never be
|
|
421
|
+
// replayed onto the turn door, the events ingress or a deliberation.
|
|
422
|
+
`epn:${requestId}`);
|
|
364
423
|
}
|
|
365
424
|
/** Publish the next world revision. Existing world INSTANCES keep the
|
|
366
425
|
* revision they were created on — a published change reaches new instances
|
|
@@ -861,12 +920,14 @@ export class PouchyWorldClient {
|
|
|
861
920
|
? String(parsed.error)
|
|
862
921
|
: undefined;
|
|
863
922
|
const retryAfterSec = retryAfterFrom(response, parsed && typeof parsed === 'object' ? parsed : null);
|
|
923
|
+
const rejectedEffects = rejectedEffectsFrom(parsed);
|
|
864
924
|
throw new WorldApiError({
|
|
865
925
|
code: codeForStatus(response.status),
|
|
866
926
|
status: response.status,
|
|
867
927
|
message: `${method} ${path} → ${response.status}`,
|
|
868
928
|
...(detail !== undefined ? { detail } : {}),
|
|
869
|
-
...(retryAfterSec !== undefined ? { retryAfterSec } : {})
|
|
929
|
+
...(retryAfterSec !== undefined ? { retryAfterSec } : {}),
|
|
930
|
+
...(rejectedEffects !== undefined ? { rejectedEffects } : {})
|
|
870
931
|
});
|
|
871
932
|
}
|
|
872
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.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",
|