@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 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, author |
60
- | `adminToken` (Firebase ID token, ~1h) | everything above plus authoring and the content loop | outlive the hour |
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. `adminToken` is for a person, or for a script a
64
- person is watching.
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=… POUCHY_ADMIN_TOKEN=… POUCHY_SECRET_KEY=… \
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
- adminToken: env('POUCHY_ADMIN_TOKEN'),
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.10.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 world READ. Prefers the machine lane when an admin key is present.
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 read through two doors; the
769
- * server answers both from one shared function, so which door you came in
770
- * by does not change the answer. The admin door drops `projectId` from the
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.10.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.owner('GET', `/projects/${this.projectId}/story-packages`);
247
+ return this.machine('GET', `/projects/${this.projectId}/story-packages`, '/admin/story-packages');
248
248
  }
249
249
  createStoryPackage(content) {
250
- return this.owner('POST', `/projects/${this.projectId}/story-packages`, content);
250
+ return this.machine('POST', `/projects/${this.projectId}/story-packages`, '/admin/story-packages', content);
251
251
  }
252
252
  getStoryPackage(packageId) {
253
- return this.owner('GET', `/projects/${this.projectId}/story-packages/${packageId}`);
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.owner('PATCH', `/projects/${this.projectId}/story-packages/${packageId}`, content);
258
+ return this.machine('PATCH', `/projects/${this.projectId}/story-packages/${packageId}`, `/admin/story-packages/${packageId}`, content);
259
259
  }
260
260
  listWorlds() {
261
- return this.owner('GET', `/projects/${this.projectId}/environments`);
261
+ return this.machine('GET', `/projects/${this.projectId}/environments`, '/admin/environments');
262
262
  }
263
263
  createWorld(definition) {
264
- return this.owner('POST', `/projects/${this.projectId}/environments`, definition);
264
+ return this.machine('POST', `/projects/${this.projectId}/environments`, '/admin/environments', definition);
265
265
  }
266
266
  getWorld(environmentId) {
267
- return this.owner('GET', `/projects/${this.projectId}/environments/${environmentId}`);
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.owner('PATCH', `/projects/${this.projectId}/environments/${environmentId}`, definition);
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: { environment: input.environment, instance: input.worldInstance },
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 world READ. Prefers the machine lane when an admin key is present.
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 read through two doors; the
536
- * server answers both from one shared function, so which door you came in
537
- * by does not change the answer. The admin door drops `projectId` from the
538
- * path because the key already names the project. */
539
- async read(ownerPath, adminPath, query = '') {
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('GET', `${adminPath}${query}`, {
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('GET', `${ownerPath}${query}`);
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.10.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",