@pouchy_ai/world-sdk 0.9.0 → 0.11.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 +53 -0
- package/README.md +46 -0
- package/dist/index.d.ts +46 -2
- package/dist/index.js +33 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# @pouchy_ai/world-sdk
|
|
2
2
|
|
|
3
|
+
## 0.11.0
|
|
4
|
+
|
|
5
|
+
Additive: your backend can tell the world what it already knows. World API 1.8.
|
|
6
|
+
|
|
7
|
+
- `runTurn` and `sendEvent` both accept `proposedPatches` — PROVIDER-authored
|
|
8
|
+
state changes committed with the beat. The server has accepted them on the
|
|
9
|
+
turns door since Batch 5; the SDK never exposed the field, and the event lane
|
|
10
|
+
never carried one at all. So a fact your system holds ("the payment cleared",
|
|
11
|
+
"the shipment arrived", "the player really does have the key") could only WAKE
|
|
12
|
+
the characters and hope one of them proposed the right effect.
|
|
13
|
+
- They ride the coordinator's single commit alongside the roles' settled
|
|
14
|
+
effects, and are validated server-side against the closed StatePatch union and
|
|
15
|
+
the pinned story package — all-or-nothing, so a batch with one bad op commits
|
|
16
|
+
nothing. A provider cannot write a role's private notes.
|
|
17
|
+
- COORDINATED worlds only. An actor world wakes each role separately and has no
|
|
18
|
+
single commit for a deterministic write to ride, so sending patches to one is
|
|
19
|
+
**refused with 422** rather than silently dropped. That refusal is the feature:
|
|
20
|
+
a channel that accepts input and discards it is the failure mode this project
|
|
21
|
+
has recorded three times, and it always reads as success.
|
|
22
|
+
- `agent.event_reply` webhooks from a coordinated world now carry `turnId`,
|
|
23
|
+
`stateRevision` and `sequence` in the `world` block. Until now a consumer got
|
|
24
|
+
the instance and the role and nothing else — it could not join the reply back
|
|
25
|
+
to the ledger turn, could not tell whether the world had moved, and could not
|
|
26
|
+
order two lines of one beat, since webhook delivery is per-message and
|
|
27
|
+
promises no ordering. All three are additive.
|
|
28
|
+
- No existing method changed shape.
|
|
29
|
+
|
|
30
|
+
## 0.10.0
|
|
31
|
+
|
|
32
|
+
Additive: an unattended backend can now read its own world. World API 1.7.
|
|
33
|
+
|
|
34
|
+
- New `adminKey` client option — a project admin key (`pak_…`), minted once from
|
|
35
|
+
the dashboard, long-lived and machine-held. When present, the world READS
|
|
36
|
+
(`getWorldState`, `listTurns`, `listTurnsSince`, `getTurn`, `getWorldMetrics`,
|
|
37
|
+
`listDeliveries`) go to a new `/v1/admin/environments/**` mirror instead of the
|
|
38
|
+
owner plane.
|
|
39
|
+
- Why this exists: `adminToken` is a Firebase ID token — roughly an hour of life,
|
|
40
|
+
minted by a browser sign-in, with no API-key path. A backend could DRIVE a turn
|
|
41
|
+
with its own credentials (Secret Key + provider signature) and then needed a
|
|
42
|
+
human at a browser to find out what the turn did. Resuming after a crash was
|
|
43
|
+
the same story. That was the single thing keeping a sixty-minute integration
|
|
44
|
+
out of reach, and it was a credential gap, not a missing capability.
|
|
45
|
+
- The mirror is reads only, and every route is GET. Driving a turn still needs
|
|
46
|
+
`secretKey` + `signing`: an admin key proves the PROJECT, never the Provider,
|
|
47
|
+
and the world's turn door requires both proofs. The delivery queue ACTIONS
|
|
48
|
+
(requeue, rehydrate, resolve-gap, drain), ledger archival, and the content loop
|
|
49
|
+
stay on `adminToken` — each decides something about a real reader's experience
|
|
50
|
+
of a story, or shortens the committed record.
|
|
51
|
+
- Both doors answer from one shared server-side read, so which credential you
|
|
52
|
+
hold does not change the answer.
|
|
53
|
+
- No existing method changed shape, and `adminToken` keeps working exactly as
|
|
54
|
+
before.
|
|
55
|
+
|
|
3
56
|
## 0.9.0
|
|
4
57
|
|
|
5
58
|
Additive: the read-back becomes a real recovery path, and the timeline gets a
|
package/README.md
CHANGED
|
@@ -50,6 +50,50 @@ const world = new PouchyWorldClient({
|
|
|
50
50
|
**Reading** — `getWorldState`, `getTurn`, `listTurns`, `listTurnsSince`,
|
|
51
51
|
`replayLedger`, `replayLedgerToEnd`.
|
|
52
52
|
|
|
53
|
+
**Which credential does what.** This matters more than it looks: one of them
|
|
54
|
+
expires within the hour.
|
|
55
|
+
|
|
56
|
+
| you hold | you can | you cannot |
|
|
57
|
+
|---|---|---|
|
|
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 |
|
|
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.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const world = new PouchyWorldClient({
|
|
68
|
+
projectId: process.env.POUCHY_PROJECT_ID!,
|
|
69
|
+
secretKey: process.env.POUCHY_SECRET_KEY!, // drive
|
|
70
|
+
adminKey: process.env.POUCHY_ADMIN_KEY!, // read back — does not expire
|
|
71
|
+
signing: { source: 'drama-backend', keyId: …, secret: … }
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
An admin key proves the PROJECT, never the Provider, so it cannot drive a turn —
|
|
76
|
+
the turn door requires a Secret Key *and* a signature over the exact bytes. And
|
|
77
|
+
the delivery queue ACTIONS stay on `adminToken` on purpose: requeue, rehydrate
|
|
78
|
+
and resolve-gap each decide what happens to a reader who is missing a beat, and
|
|
79
|
+
resolve-gap tells them it is never coming.
|
|
80
|
+
|
|
81
|
+
**Telling the world a fact you already hold.** Your system knows things the
|
|
82
|
+
model can only guess at. Send them as patches and the world records them,
|
|
83
|
+
instead of hoping a character proposes the right effect:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
await world.runTurn({
|
|
87
|
+
environmentId, worldInstanceId,
|
|
88
|
+
text: 'I hand over the coin pouch.',
|
|
89
|
+
proposedPatches: [{ op: 'set_flag', key: 'paid', value: true }]
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`sendEvent` takes the same field. Coordinated worlds only — an actor world has
|
|
94
|
+
no single commit for a deterministic write to ride, and sending patches to one
|
|
95
|
+
answers 422 rather than dropping them quietly.
|
|
96
|
+
|
|
53
97
|
**Resuming after a crash.** `getTurn` returns the same fields the live result
|
|
54
98
|
did — `nextOptions` included — so a recovered session can offer the audience
|
|
55
99
|
the choices it was about to. To catch up on beats you missed entirely, store
|
|
@@ -150,6 +194,8 @@ and exits non-zero on the first structural failure.
|
|
|
150
194
|
|
|
151
195
|
- `docs/world-quickstart-drama.md` — screenplay → world → new screenplay draft
|
|
152
196
|
- `docs/world-quickstart-npc.md` — one town, three NPCs, one shared state
|
|
197
|
+
- `docs/world-quickstart-nextjs.md` — a Next.js app: one route handler, one page,
|
|
198
|
+
no browser login anywhere in the deployment
|
|
153
199
|
|
|
154
200
|
## License
|
|
155
201
|
|
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.11.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. */
|
|
@@ -474,8 +474,23 @@ export interface WorldClientOptions {
|
|
|
474
474
|
/** The project this client acts for. */
|
|
475
475
|
projectId: string;
|
|
476
476
|
/** An OWNER-plane credential (a signed-in admin's ID token) for the control
|
|
477
|
-
* plane: story packages, world definitions, drafts, replay, reads.
|
|
477
|
+
* plane: story packages, world definitions, drafts, replay, reads.
|
|
478
|
+
*
|
|
479
|
+
* Short-lived — a Firebase ID token, roughly an hour — and minted by a
|
|
480
|
+
* browser sign-in. Fine for a script a person is watching; wrong for a
|
|
481
|
+
* server. For a server, use `adminKey`. */
|
|
478
482
|
adminToken?: string;
|
|
483
|
+
/** A project ADMIN key (`pak_…`), minted once from the dashboard.
|
|
484
|
+
*
|
|
485
|
+
* Long-lived and machine-held: this is what an unattended backend uses.
|
|
486
|
+
* When present, the world READS (overview, state, timeline, turn read-back,
|
|
487
|
+
* metrics, deliveries) go to the `/admin` mirror instead of the owner
|
|
488
|
+
* plane, and no browser login is involved anywhere in your deployment.
|
|
489
|
+
*
|
|
490
|
+
* It proves the PROJECT, never the Provider, so it cannot drive a turn —
|
|
491
|
+
* that still needs `secretKey` + `signing`. Authoring calls and the content
|
|
492
|
+
* loop still need `adminToken`; see the README for which is which. */
|
|
493
|
+
adminKey?: string;
|
|
479
494
|
/** A project Secret Key (`pchy_sk_…`) for the MACHINE lane: world sessions,
|
|
480
495
|
* turns and trusted events. It also carries the test/live axis. */
|
|
481
496
|
secretKey?: string;
|
|
@@ -497,6 +512,7 @@ export declare class PouchyWorldClient {
|
|
|
497
512
|
private readonly baseUrl;
|
|
498
513
|
private readonly doFetch;
|
|
499
514
|
private readonly adminToken?;
|
|
515
|
+
private readonly adminKey?;
|
|
500
516
|
private readonly secretKey?;
|
|
501
517
|
private readonly signing?;
|
|
502
518
|
private readonly timeoutMs;
|
|
@@ -734,6 +750,19 @@ export declare class PouchyWorldClient {
|
|
|
734
750
|
text: string;
|
|
735
751
|
sourceRoleId?: string;
|
|
736
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>[];
|
|
737
766
|
}): Promise<WorldTurnResult>;
|
|
738
767
|
/** Send a trusted EVENT into a world. On a `coordinated` world this becomes
|
|
739
768
|
* one coordinator turn; on an `actor` world it wakes each subscribed role.
|
|
@@ -746,7 +775,22 @@ export declare class PouchyWorldClient {
|
|
|
746
775
|
data: Record<string, unknown>;
|
|
747
776
|
schemaVersion?: number;
|
|
748
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>[];
|
|
749
786
|
}): Promise<Record<string, unknown>>;
|
|
787
|
+
/** A world READ. Prefers the machine lane when an admin key is present.
|
|
788
|
+
*
|
|
789
|
+
* `ownerPath` and `adminPath` address the same read through two doors; the
|
|
790
|
+
* server answers both from one shared function, so which door you came in
|
|
791
|
+
* by does not change the answer. The admin door drops `projectId` from the
|
|
792
|
+
* path because the key already names the project. */
|
|
793
|
+
private read;
|
|
750
794
|
private owner;
|
|
751
795
|
/** The machine lane: Secret Key AND a source signature over the EXACT bytes
|
|
752
796
|
* being sent — the two proofs the world requires of a backend. */
|
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.11.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
|
|
@@ -222,6 +222,7 @@ export class PouchyWorldClient {
|
|
|
222
222
|
baseUrl;
|
|
223
223
|
doFetch;
|
|
224
224
|
adminToken;
|
|
225
|
+
adminKey;
|
|
225
226
|
secretKey;
|
|
226
227
|
signing;
|
|
227
228
|
timeoutMs;
|
|
@@ -233,6 +234,8 @@ export class PouchyWorldClient {
|
|
|
233
234
|
this.doFetch = options.fetch ?? globalThis.fetch;
|
|
234
235
|
if (options.adminToken !== undefined)
|
|
235
236
|
this.adminToken = options.adminToken;
|
|
237
|
+
if (options.adminKey !== undefined)
|
|
238
|
+
this.adminKey = options.adminKey;
|
|
236
239
|
if (options.secretKey !== undefined)
|
|
237
240
|
this.secretKey = options.secretKey;
|
|
238
241
|
if (options.signing !== undefined)
|
|
@@ -271,7 +274,7 @@ export class PouchyWorldClient {
|
|
|
271
274
|
return this.owner('PATCH', `/projects/${this.projectId}/environments/${environmentId}`, definition);
|
|
272
275
|
}
|
|
273
276
|
getWorldState(environmentId, worldInstanceId) {
|
|
274
|
-
return this.
|
|
277
|
+
return this.read(`/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/state`, `/admin/environments/${environmentId}/instances/${worldInstanceId}/state`);
|
|
275
278
|
}
|
|
276
279
|
/** Read back a COMMITTED turn. The recovery path when a response was lost:
|
|
277
280
|
* it re-runs nothing, and a 404 means the turn never committed. */
|
|
@@ -282,7 +285,7 @@ export class PouchyWorldClient {
|
|
|
282
285
|
* same choices it would have. Rows committed before that carry none of the
|
|
283
286
|
* four turn-time facts: absent means UNKNOWN, never "none". */
|
|
284
287
|
getTurn(environmentId, worldInstanceId, turnId) {
|
|
285
|
-
return this.
|
|
288
|
+
return this.read(`/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/turns/${encodeURIComponent(turnId)}`, `/admin/environments/${environmentId}/instances/${worldInstanceId}/turns/${encodeURIComponent(turnId)}`);
|
|
286
289
|
}
|
|
287
290
|
/** Verify that the materialized state is what the committed ledger says.
|
|
288
291
|
* Always a dry run — it reports, it never repairs. Pass the previous
|
|
@@ -300,7 +303,7 @@ export class PouchyWorldClient {
|
|
|
300
303
|
if (opts?.sinceSeq !== undefined)
|
|
301
304
|
q.set('sinceSeq', String(opts.sinceSeq));
|
|
302
305
|
const suffix = q.toString() ? `?${q}` : '';
|
|
303
|
-
return this.
|
|
306
|
+
return this.read(`/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/turns`, `/admin/environments/${environmentId}/instances/${worldInstanceId}/turns`, suffix);
|
|
304
307
|
}
|
|
305
308
|
/** Walk the timeline FORWARD from a revision already processed, following
|
|
306
309
|
* the cursor to the end. The resume primitive: a backend that stored the
|
|
@@ -355,7 +358,7 @@ export class PouchyWorldClient {
|
|
|
355
358
|
if (v !== undefined)
|
|
356
359
|
q.set(k, String(v));
|
|
357
360
|
const suffix = q.toString() ? `?${q}` : '';
|
|
358
|
-
return this.
|
|
361
|
+
return this.read(`/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/deliveries`, `/admin/environments/${environmentId}/instances/${worldInstanceId}/deliveries`, suffix);
|
|
359
362
|
}
|
|
360
363
|
/** One delivery. `includePayload` returns the line itself and writes an audit
|
|
361
364
|
* row naming you — pass it deliberately, not by default. */
|
|
@@ -411,7 +414,7 @@ export class PouchyWorldClient {
|
|
|
411
414
|
* kept apart because "the lines arrived" and "the world played well" are
|
|
412
415
|
* different questions. */
|
|
413
416
|
getWorldMetrics(environmentId, worldInstanceId) {
|
|
414
|
-
return this.
|
|
417
|
+
return this.read(`/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/metrics`, `/admin/environments/${environmentId}/instances/${worldInstanceId}/metrics`);
|
|
415
418
|
}
|
|
416
419
|
/** Score the committed history against a quality suite. Deterministic — no
|
|
417
420
|
* model judges the output, so a score is something you can regress. */
|
|
@@ -505,7 +508,8 @@ export class PouchyWorldClient {
|
|
|
505
508
|
trigger: {
|
|
506
509
|
kind: 'user',
|
|
507
510
|
text: input.text,
|
|
508
|
-
...(input.sourceRoleId ? { sourceRoleId: input.sourceRoleId } : {})
|
|
511
|
+
...(input.sourceRoleId ? { sourceRoleId: input.sourceRoleId } : {}),
|
|
512
|
+
...(input.proposedPatches ? { proposedPatches: input.proposedPatches } : {})
|
|
509
513
|
},
|
|
510
514
|
...(input.traceId ? { traceId: input.traceId } : {})
|
|
511
515
|
};
|
|
@@ -521,15 +525,35 @@ export class PouchyWorldClient {
|
|
|
521
525
|
eventId,
|
|
522
526
|
schemaVersion: input.schemaVersion ?? 1,
|
|
523
527
|
data: input.data,
|
|
524
|
-
world: {
|
|
528
|
+
world: {
|
|
529
|
+
environment: input.environment,
|
|
530
|
+
instance: input.worldInstance,
|
|
531
|
+
...(input.proposedPatches ? { proposedPatches: input.proposedPatches } : {})
|
|
532
|
+
},
|
|
525
533
|
...(input.occurredAt !== undefined ? { occurredAt: input.occurredAt } : {})
|
|
526
534
|
};
|
|
527
535
|
return this.signed(`/projects/${this.projectId}/events`, body, eventId);
|
|
528
536
|
}
|
|
529
537
|
// ── transport ────────────────────────────────────────────────────────────
|
|
538
|
+
/** A world READ. Prefers the machine lane when an admin key is present.
|
|
539
|
+
*
|
|
540
|
+
* `ownerPath` and `adminPath` address the same read through two doors; the
|
|
541
|
+
* server answers both from one shared function, so which door you came in
|
|
542
|
+
* by does not change the answer. The admin door drops `projectId` from the
|
|
543
|
+
* path because the key already names the project. */
|
|
544
|
+
async read(ownerPath, adminPath, query = '') {
|
|
545
|
+
if (this.adminKey) {
|
|
546
|
+
return this.request('GET', `${adminPath}${query}`, {
|
|
547
|
+
headers: { authorization: `Bearer ${this.adminKey}` }
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
return this.owner('GET', `${ownerPath}${query}`);
|
|
551
|
+
}
|
|
530
552
|
async owner(method, path, body) {
|
|
531
553
|
if (!this.adminToken) {
|
|
532
|
-
throw new Error(`${method} ${path} needs an owner token (adminToken)`
|
|
554
|
+
throw new Error(`${method} ${path} needs an owner token (adminToken) — or, for a world READ ` +
|
|
555
|
+
`from an unattended backend, a project admin key (adminKey), which routes ` +
|
|
556
|
+
`the read through the /admin mirror and does not expire`);
|
|
533
557
|
}
|
|
534
558
|
return this.request(method, path, {
|
|
535
559
|
headers: { authorization: `Bearer ${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.11.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",
|