@pouchy_ai/world-sdk 0.10.0 → 0.12.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 +50 -0
- package/README.md +24 -5
- package/conformance.mjs +12 -2
- package/dist/index.d.ts +38 -6
- package/dist/index.js +39 -20
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# @pouchy_ai/world-sdk
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
Additive: authoring joins the machine lane, so nothing in an integration needs
|
|
6
|
+
a browser.
|
|
7
|
+
|
|
8
|
+
- `createStoryPackage`, `publishStoryPackage`, `getStoryPackage`,
|
|
9
|
+
`listStoryPackages`, `createWorld`, `publishWorld`, `getWorld` and
|
|
10
|
+
`listWorlds` now go to the `/v1/admin` plane when `adminKey` is set, exactly
|
|
11
|
+
as the reads have since 0.10.0.
|
|
12
|
+
- These admin equivalents have existed since Batch 1. The SDK simply never used
|
|
13
|
+
them, so authoring kept demanding `adminToken` — a Firebase ID token, minted
|
|
14
|
+
by a browser sign-in and good for about an hour — for operations a project
|
|
15
|
+
admin key could always perform.
|
|
16
|
+
- **`conformance.mjs` runs on machine credentials now.** That is what this
|
|
17
|
+
release is for. The one check that proves a project's world plane is wired
|
|
18
|
+
correctly could not run without a person opening a browser first, which is
|
|
19
|
+
precisely what the rest of this SDK stopped requiring in 0.10.0. Set
|
|
20
|
+
`POUCHY_ADMIN_KEY`; `POUCHY_ADMIN_TOKEN` still works if you prefer it.
|
|
21
|
+
- `adminToken` remains supported and unchanged. It is still the only credential
|
|
22
|
+
for the content loop (script drafts, editorial review), where a person is the
|
|
23
|
+
point rather than an obstacle.
|
|
24
|
+
- No method changed shape.
|
|
25
|
+
|
|
26
|
+
## 0.11.0
|
|
27
|
+
|
|
28
|
+
Additive: your backend can tell the world what it already knows. World API 1.8.
|
|
29
|
+
|
|
30
|
+
- `runTurn` and `sendEvent` both accept `proposedPatches` — PROVIDER-authored
|
|
31
|
+
state changes committed with the beat. The server has accepted them on the
|
|
32
|
+
turns door since Batch 5; the SDK never exposed the field, and the event lane
|
|
33
|
+
never carried one at all. So a fact your system holds ("the payment cleared",
|
|
34
|
+
"the shipment arrived", "the player really does have the key") could only WAKE
|
|
35
|
+
the characters and hope one of them proposed the right effect.
|
|
36
|
+
- They ride the coordinator's single commit alongside the roles' settled
|
|
37
|
+
effects, and are validated server-side against the closed StatePatch union and
|
|
38
|
+
the pinned story package — all-or-nothing, so a batch with one bad op commits
|
|
39
|
+
nothing. A provider cannot write a role's private notes.
|
|
40
|
+
- COORDINATED worlds only. An actor world wakes each role separately and has no
|
|
41
|
+
single commit for a deterministic write to ride, so sending patches to one is
|
|
42
|
+
**refused with 422** rather than silently dropped. That refusal is the feature:
|
|
43
|
+
a channel that accepts input and discards it is the failure mode this project
|
|
44
|
+
has recorded three times, and it always reads as success.
|
|
45
|
+
- `agent.event_reply` webhooks from a coordinated world now carry `turnId`,
|
|
46
|
+
`stateRevision` and `sequence` in the `world` block. Until now a consumer got
|
|
47
|
+
the instance and the role and nothing else — it could not join the reply back
|
|
48
|
+
to the ledger turn, could not tell whether the world had moved, and could not
|
|
49
|
+
order two lines of one beat, since webhook delivery is per-message and
|
|
50
|
+
promises no ordering. All three are additive.
|
|
51
|
+
- No existing method changed shape.
|
|
52
|
+
|
|
3
53
|
## 0.10.0
|
|
4
54
|
|
|
5
55
|
Additive: an unattended backend can now read its own world. World API 1.7.
|
package/README.md
CHANGED
|
@@ -56,12 +56,13 @@ expires within the hour.
|
|
|
56
56
|
| you hold | you can | you cannot |
|
|
57
57
|
|---|---|---|
|
|
58
58
|
| `secretKey` + `signing` | mint sessions, drive turns, send events | read anything back |
|
|
59
|
-
| `adminKey` (`pak_…`, long-lived) | read the world: overview, state, timeline, turn read-back, metrics, delivery queue | drive a turn, act on the queue,
|
|
60
|
-
| `adminToken` (Firebase ID token, ~1h) | everything above plus
|
|
59
|
+
| `adminKey` (`pak_…`, long-lived) | author story packages and worlds; read the world: overview, state, timeline, turn read-back, metrics, delivery queue, cost | drive a turn, act on the delivery queue, run the content loop |
|
|
60
|
+
| `adminToken` (Firebase ID token, ~1h) | everything above plus the content loop | outlive the hour |
|
|
61
61
|
|
|
62
|
-
A server holds `secretKey` + `signing` + `adminKey` and needs no browser login
|
|
63
|
-
anywhere in its deployment
|
|
64
|
-
person is
|
|
62
|
+
A server holds `secretKey` + `signing` + `adminKey` and needs **no browser login
|
|
63
|
+
anywhere in its deployment** — including `conformance.mjs`, which used to demand
|
|
64
|
+
one. `adminToken` is for a person: the content loop's review step is a human
|
|
65
|
+
decision, which is the point of it rather than an obstacle.
|
|
65
66
|
|
|
66
67
|
```ts
|
|
67
68
|
const world = new PouchyWorldClient({
|
|
@@ -78,6 +79,22 @@ the delivery queue ACTIONS stay on `adminToken` on purpose: requeue, rehydrate
|
|
|
78
79
|
and resolve-gap each decide what happens to a reader who is missing a beat, and
|
|
79
80
|
resolve-gap tells them it is never coming.
|
|
80
81
|
|
|
82
|
+
**Telling the world a fact you already hold.** Your system knows things the
|
|
83
|
+
model can only guess at. Send them as patches and the world records them,
|
|
84
|
+
instead of hoping a character proposes the right effect:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
await world.runTurn({
|
|
88
|
+
environmentId, worldInstanceId,
|
|
89
|
+
text: 'I hand over the coin pouch.',
|
|
90
|
+
proposedPatches: [{ op: 'set_flag', key: 'paid', value: true }]
|
|
91
|
+
});
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`sendEvent` takes the same field. Coordinated worlds only — an actor world has
|
|
95
|
+
no single commit for a deterministic write to ride, and sending patches to one
|
|
96
|
+
answers 422 rather than dropping them quietly.
|
|
97
|
+
|
|
81
98
|
**Resuming after a crash.** `getTurn` returns the same fields the live result
|
|
82
99
|
did — `nextOptions` included — so a recovered session can offer the audience
|
|
83
100
|
the choices it was about to. To catch up on beats you missed entirely, store
|
|
@@ -178,6 +195,8 @@ and exits non-zero on the first structural failure.
|
|
|
178
195
|
|
|
179
196
|
- `docs/world-quickstart-drama.md` — screenplay → world → new screenplay draft
|
|
180
197
|
- `docs/world-quickstart-npc.md` — one town, three NPCs, one shared state
|
|
198
|
+
- `docs/world-quickstart-nextjs.md` — a Next.js app: one route handler, one page,
|
|
199
|
+
no browser login anywhere in the deployment
|
|
181
200
|
|
|
182
201
|
## License
|
|
183
202
|
|
package/conformance.mjs
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// has to answer AND touch world state, which is the failure mode that matters
|
|
21
21
|
// there (an NPC that chats without the world moving looks fine and is not).
|
|
22
22
|
//
|
|
23
|
-
// POUCHY_PROJECT_ID=…
|
|
23
|
+
// POUCHY_PROJECT_ID=… POUCHY_ADMIN_KEY=… POUCHY_SECRET_KEY=… \
|
|
24
24
|
// POUCHY_SOURCE=… POUCHY_SOURCE_KID=… POUCHY_SOURCE_SECRET=… \
|
|
25
25
|
// POUCHY_AGENT_A=… POUCHY_AGENT_B=… node conformance.mjs [--scenario=drama|npc]
|
|
26
26
|
|
|
@@ -114,7 +114,17 @@ async function check(name, fn) {
|
|
|
114
114
|
|
|
115
115
|
const world = new PouchyWorldClient({
|
|
116
116
|
projectId: env('POUCHY_PROJECT_ID'),
|
|
117
|
-
|
|
117
|
+
// Batch 10: an ADMIN KEY is enough, and is the point.
|
|
118
|
+
//
|
|
119
|
+
// This harness used to demand POUCHY_ADMIN_TOKEN — a Firebase ID token,
|
|
120
|
+
// browser-minted and good for about an hour. So the one check that proves a
|
|
121
|
+
// project's world plane is wired correctly could not run without a person
|
|
122
|
+
// opening a browser first, which is exactly the thing the rest of this SDK
|
|
123
|
+
// stopped requiring. Either credential works now; the key is preferred and
|
|
124
|
+
// needs no human.
|
|
125
|
+
...(process.env.POUCHY_ADMIN_KEY
|
|
126
|
+
? { adminKey: process.env.POUCHY_ADMIN_KEY }
|
|
127
|
+
: { adminToken: env('POUCHY_ADMIN_TOKEN') }),
|
|
118
128
|
secretKey: env('POUCHY_SECRET_KEY'),
|
|
119
129
|
signing: {
|
|
120
130
|
source: env('POUCHY_SOURCE'),
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const WORLD_SDK_VERSION = "0.
|
|
1
|
+
export declare const WORLD_SDK_VERSION = "0.12.0";
|
|
2
2
|
export declare const DEFAULT_BASE_URL = "https://pouchy.ai/v1";
|
|
3
3
|
/** The refusal classes a world call can produce. `unknown` is deliberate: an
|
|
4
4
|
* unrecognized status is never quietly folded into a neighbour. */
|
|
@@ -750,6 +750,19 @@ export declare class PouchyWorldClient {
|
|
|
750
750
|
text: string;
|
|
751
751
|
sourceRoleId?: string;
|
|
752
752
|
traceId?: string;
|
|
753
|
+
/** PROVIDER-authored state changes, committed with this beat.
|
|
754
|
+
*
|
|
755
|
+
* Your backend already knows things the model can only guess at — the
|
|
756
|
+
* payment cleared, the shipment arrived, the player actually holds the
|
|
757
|
+
* key. Send them as patches and the world HOLDS them, rather than
|
|
758
|
+
* hoping a character proposes the right effect.
|
|
759
|
+
*
|
|
760
|
+
* They ride the coordinator's single commit alongside the roles'
|
|
761
|
+
* settled effects, and are validated server-side against the closed
|
|
762
|
+
* StatePatch union and the pinned story package — all-or-nothing, so a
|
|
763
|
+
* batch with one bad op commits nothing. A provider cannot write a
|
|
764
|
+
* role's private notes. */
|
|
765
|
+
proposedPatches?: readonly Record<string, unknown>[];
|
|
753
766
|
}): Promise<WorldTurnResult>;
|
|
754
767
|
/** Send a trusted EVENT into a world. On a `coordinated` world this becomes
|
|
755
768
|
* one coordinator turn; on an `actor` world it wakes each subscribed role.
|
|
@@ -762,13 +775,32 @@ export declare class PouchyWorldClient {
|
|
|
762
775
|
data: Record<string, unknown>;
|
|
763
776
|
schemaVersion?: number;
|
|
764
777
|
occurredAt?: number;
|
|
778
|
+
/** PROVIDER-authored state changes to commit with the beat this event
|
|
779
|
+
* produces. Same contract as `runTurn`'s.
|
|
780
|
+
*
|
|
781
|
+
* COORDINATED worlds only. An actor world wakes each role separately
|
|
782
|
+
* and has no single commit for a deterministic write to ride, so
|
|
783
|
+
* sending these to one is refused with 422 rather than silently
|
|
784
|
+
* dropped — you will hear about it, which is the point. */
|
|
785
|
+
proposedPatches?: readonly Record<string, unknown>[];
|
|
765
786
|
}): Promise<Record<string, unknown>>;
|
|
766
|
-
/** A
|
|
787
|
+
/** A call that BOTH planes serve. Prefers the machine lane when an admin key
|
|
788
|
+
* is present.
|
|
767
789
|
*
|
|
768
|
-
* `ownerPath` and `adminPath` address the same
|
|
769
|
-
* server answers both from one shared
|
|
770
|
-
* by does not change the answer. The admin door drops
|
|
771
|
-
* path because the key already names the project
|
|
790
|
+
* `ownerPath` and `adminPath` address the same operation through two doors,
|
|
791
|
+
* and the server answers both from one shared implementation — so which
|
|
792
|
+
* door you came in by does not change the answer. The admin door drops
|
|
793
|
+
* `projectId` from the path because the key already names the project,
|
|
794
|
+
* which is also what makes it unable to address another project's work.
|
|
795
|
+
*
|
|
796
|
+
* Batch 10 PR-B2.1 widened this from reads to AUTHORING. The admin
|
|
797
|
+
* equivalents for story packages and world definitions have existed since
|
|
798
|
+
* Batch 1; the SDK simply never used them, which left `createStoryPackage`
|
|
799
|
+
* and friends demanding an hour-lived browser token for an operation a
|
|
800
|
+
* machine key could always perform. `conformance.mjs` is the proof: it
|
|
801
|
+
* could not run without a person opening a browser first. */
|
|
802
|
+
private machine;
|
|
803
|
+
/** A world READ through whichever door the caller holds. */
|
|
772
804
|
private read;
|
|
773
805
|
private owner;
|
|
774
806
|
/** The machine lane: Secret Key AND a source signature over the EXACT bytes
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// SIGNING a request the way the server verifies it, and choosing turn ids that
|
|
18
18
|
// make a retry idempotent instead of a second beat.
|
|
19
19
|
import { createHash, createHmac, randomUUID } from 'node:crypto';
|
|
20
|
-
export const WORLD_SDK_VERSION = '0.
|
|
20
|
+
export const WORLD_SDK_VERSION = '0.12.0';
|
|
21
21
|
export const DEFAULT_BASE_URL = 'https://pouchy.ai/v1';
|
|
22
22
|
// ── errors ─────────────────────────────────────────────────────────────────
|
|
23
23
|
/** The refusal classes a world call can produce. `unknown` is deliberate: an
|
|
@@ -244,34 +244,34 @@ export class PouchyWorldClient {
|
|
|
244
244
|
}
|
|
245
245
|
// ── control plane (owner token) ──────────────────────────────────────────
|
|
246
246
|
listStoryPackages() {
|
|
247
|
-
return this.
|
|
247
|
+
return this.machine('GET', `/projects/${this.projectId}/story-packages`, '/admin/story-packages');
|
|
248
248
|
}
|
|
249
249
|
createStoryPackage(content) {
|
|
250
|
-
return this.
|
|
250
|
+
return this.machine('POST', `/projects/${this.projectId}/story-packages`, '/admin/story-packages', content);
|
|
251
251
|
}
|
|
252
252
|
getStoryPackage(packageId) {
|
|
253
|
-
return this.
|
|
253
|
+
return this.machine('GET', `/projects/${this.projectId}/story-packages/${packageId}`, `/admin/story-packages/${packageId}`);
|
|
254
254
|
}
|
|
255
255
|
/** Publish the next IMMUTABLE story revision. Idempotent on content: the
|
|
256
256
|
* same bytes return the existing revision rather than minting a twin. */
|
|
257
257
|
publishStoryPackage(packageId, content) {
|
|
258
|
-
return this.
|
|
258
|
+
return this.machine('PATCH', `/projects/${this.projectId}/story-packages/${packageId}`, `/admin/story-packages/${packageId}`, content);
|
|
259
259
|
}
|
|
260
260
|
listWorlds() {
|
|
261
|
-
return this.
|
|
261
|
+
return this.machine('GET', `/projects/${this.projectId}/environments`, '/admin/environments');
|
|
262
262
|
}
|
|
263
263
|
createWorld(definition) {
|
|
264
|
-
return this.
|
|
264
|
+
return this.machine('POST', `/projects/${this.projectId}/environments`, '/admin/environments', definition);
|
|
265
265
|
}
|
|
266
266
|
getWorld(environmentId) {
|
|
267
|
-
return this.
|
|
267
|
+
return this.machine('GET', `/projects/${this.projectId}/environments/${environmentId}`, `/admin/environments/${environmentId}`);
|
|
268
268
|
}
|
|
269
269
|
/** Publish the next world revision. Existing world INSTANCES keep the
|
|
270
270
|
* revision they were created on — a published change reaches new instances
|
|
271
271
|
* only, which is what keeps a running story from changing runtime or rules
|
|
272
272
|
* underneath its players. */
|
|
273
273
|
publishWorld(environmentId, definition) {
|
|
274
|
-
return this.
|
|
274
|
+
return this.machine('PATCH', `/projects/${this.projectId}/environments/${environmentId}`, `/admin/environments/${environmentId}`, definition);
|
|
275
275
|
}
|
|
276
276
|
getWorldState(environmentId, worldInstanceId) {
|
|
277
277
|
return this.read(`/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/state`, `/admin/environments/${environmentId}/instances/${worldInstanceId}/state`);
|
|
@@ -508,7 +508,8 @@ export class PouchyWorldClient {
|
|
|
508
508
|
trigger: {
|
|
509
509
|
kind: 'user',
|
|
510
510
|
text: input.text,
|
|
511
|
-
...(input.sourceRoleId ? { sourceRoleId: input.sourceRoleId } : {})
|
|
511
|
+
...(input.sourceRoleId ? { sourceRoleId: input.sourceRoleId } : {}),
|
|
512
|
+
...(input.proposedPatches ? { proposedPatches: input.proposedPatches } : {})
|
|
512
513
|
},
|
|
513
514
|
...(input.traceId ? { traceId: input.traceId } : {})
|
|
514
515
|
};
|
|
@@ -524,25 +525,43 @@ export class PouchyWorldClient {
|
|
|
524
525
|
eventId,
|
|
525
526
|
schemaVersion: input.schemaVersion ?? 1,
|
|
526
527
|
data: input.data,
|
|
527
|
-
world: {
|
|
528
|
+
world: {
|
|
529
|
+
environment: input.environment,
|
|
530
|
+
instance: input.worldInstance,
|
|
531
|
+
...(input.proposedPatches ? { proposedPatches: input.proposedPatches } : {})
|
|
532
|
+
},
|
|
528
533
|
...(input.occurredAt !== undefined ? { occurredAt: input.occurredAt } : {})
|
|
529
534
|
};
|
|
530
535
|
return this.signed(`/projects/${this.projectId}/events`, body, eventId);
|
|
531
536
|
}
|
|
532
537
|
// ── transport ────────────────────────────────────────────────────────────
|
|
533
|
-
/** A
|
|
538
|
+
/** A call that BOTH planes serve. Prefers the machine lane when an admin key
|
|
539
|
+
* is present.
|
|
534
540
|
*
|
|
535
|
-
* `ownerPath` and `adminPath` address the same
|
|
536
|
-
* server answers both from one shared
|
|
537
|
-
* by does not change the answer. The admin door drops
|
|
538
|
-
* path because the key already names the project
|
|
539
|
-
|
|
541
|
+
* `ownerPath` and `adminPath` address the same operation through two doors,
|
|
542
|
+
* and the server answers both from one shared implementation — so which
|
|
543
|
+
* door you came in by does not change the answer. The admin door drops
|
|
544
|
+
* `projectId` from the path because the key already names the project,
|
|
545
|
+
* which is also what makes it unable to address another project's work.
|
|
546
|
+
*
|
|
547
|
+
* Batch 10 PR-B2.1 widened this from reads to AUTHORING. The admin
|
|
548
|
+
* equivalents for story packages and world definitions have existed since
|
|
549
|
+
* Batch 1; the SDK simply never used them, which left `createStoryPackage`
|
|
550
|
+
* and friends demanding an hour-lived browser token for an operation a
|
|
551
|
+
* machine key could always perform. `conformance.mjs` is the proof: it
|
|
552
|
+
* could not run without a person opening a browser first. */
|
|
553
|
+
async machine(method, ownerPath, adminPath, body, query = '') {
|
|
540
554
|
if (this.adminKey) {
|
|
541
|
-
return this.request(
|
|
542
|
-
headers: { authorization: `Bearer ${this.adminKey}` }
|
|
555
|
+
return this.request(method, `${adminPath}${query}`, {
|
|
556
|
+
headers: { authorization: `Bearer ${this.adminKey}` },
|
|
557
|
+
...(body !== undefined ? { raw: JSON.stringify(body) } : {})
|
|
543
558
|
});
|
|
544
559
|
}
|
|
545
|
-
return this.owner(
|
|
560
|
+
return this.owner(method, `${ownerPath}${query}`, body);
|
|
561
|
+
}
|
|
562
|
+
/** A world READ through whichever door the caller holds. */
|
|
563
|
+
async read(ownerPath, adminPath, query = '') {
|
|
564
|
+
return this.machine('GET', ownerPath, adminPath, undefined, query);
|
|
546
565
|
}
|
|
547
566
|
async owner(method, path, body) {
|
|
548
567
|
if (!this.adminToken) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pouchy_ai/world-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|