@pouchy_ai/world-sdk 0.8.0 → 0.9.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 +30 -0
- package/README.md +16 -1
- package/dist/index.d.ts +126 -2
- package/dist/index.js +52 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# @pouchy_ai/world-sdk
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
Additive: the read-back becomes a real recovery path, and the timeline gets a
|
|
6
|
+
resume cursor. World API 1.6.
|
|
7
|
+
|
|
8
|
+
- `getTurn` now returns a typed `WorldTurnReadback` instead of an opaque
|
|
9
|
+
record, and the server fills in the four turn-time facts the live result
|
|
10
|
+
always had and the read-back never did: `selectedRoles`, `skippedRoles`,
|
|
11
|
+
`repairs` and `nextOptions`. Recovering a lost response used to tell you what
|
|
12
|
+
the world changed and not what it offered next, which made resuming a story
|
|
13
|
+
guesswork. All four are optional — a turn committed before this release
|
|
14
|
+
carries none of them, and absence means UNKNOWN, not "none".
|
|
15
|
+
- Two divergences from `WorldTurnResult` are deliberate and now typed rather
|
|
16
|
+
than implied: a read-back's `completionStatus`/`executionStatus` are the
|
|
17
|
+
constant `'committed'` (a turn that never committed has no entry and answers
|
|
18
|
+
404), and its `deliveryStatus` adds `'unknown'` (once settled rows are
|
|
19
|
+
reaped, the absence of a queue row is not proof of delivery).
|
|
20
|
+
- New `listTurns(environmentId, worldInstanceId, { limit, sinceSeq })` returning
|
|
21
|
+
a `WorldTimelinePage`. With `sinceSeq` you get the beats AFTER that revision,
|
|
22
|
+
oldest first, plus `nextSinceSeq` to continue; without it you get the
|
|
23
|
+
newest-first window as before. The response names its own `order`.
|
|
24
|
+
- New `listTurnsSince(...)` — follows that cursor to the end. This is the resume
|
|
25
|
+
primitive: a backend that stored the last `seq` it handled gets exactly the
|
|
26
|
+
beats it missed, in order, with no gap at a page boundary and no beat twice.
|
|
27
|
+
Bounded by `maxPages`, because an unbounded follow loop against a busy world
|
|
28
|
+
never returns.
|
|
29
|
+
- The API document is now served at `GET https://pouchy.ai/v1/world/openapi` —
|
|
30
|
+
public, unauthenticated, and previously reachable by nobody.
|
|
31
|
+
- No existing method changed shape.
|
|
32
|
+
|
|
3
33
|
## 0.8.0
|
|
4
34
|
|
|
5
35
|
- `WorldDeliveryOpsRow` gained `cleanupRetryCount` and `cleanupStalledAt`.
|
package/README.md
CHANGED
|
@@ -47,7 +47,22 @@ const world = new PouchyWorldClient({
|
|
|
47
47
|
|
|
48
48
|
**Running** — `createWorldSession`, `runTurn`, `sendEvent`.
|
|
49
49
|
|
|
50
|
-
**Reading** — `getWorldState`, `getTurn`, `
|
|
50
|
+
**Reading** — `getWorldState`, `getTurn`, `listTurns`, `listTurnsSince`,
|
|
51
|
+
`replayLedger`, `replayLedgerToEnd`.
|
|
52
|
+
|
|
53
|
+
**Resuming after a crash.** `getTurn` returns the same fields the live result
|
|
54
|
+
did — `nextOptions` included — so a recovered session can offer the audience
|
|
55
|
+
the choices it was about to. To catch up on beats you missed entirely, store
|
|
56
|
+
the last `seq` you processed and call `listTurnsSince`:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const missed = await world.listTurnsSince(envId, instanceId, lastSeqIHandled);
|
|
60
|
+
for (const beat of missed) render(beat); // in order, no gap, no repeat
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Both are additive: a turn committed before world API 1.6 carries none of the
|
|
64
|
+
four turn-time facts (`selectedRoles`, `skippedRoles`, `repairs`,
|
|
65
|
+
`nextOptions`). Absent means UNKNOWN, never "none".
|
|
51
66
|
|
|
52
67
|
**Content return** — `createScriptDraft`, `getScriptDraft`, `listScriptDrafts`,
|
|
53
68
|
`reviewScriptDraft`, `exportScriptDraft`.
|
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.9.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. */
|
|
@@ -106,6 +106,102 @@ export interface WorldTurnResult {
|
|
|
106
106
|
/** Why a redacted dead letter could not be rebuilt. A closed vocabulary: an
|
|
107
107
|
* operator deciding whether to resolve a gap is making a judgement call, and a
|
|
108
108
|
* free-text reason is one they cannot compare across incidents. */
|
|
109
|
+
/** What `GET …/turns/{turnId}` answers. The live result's fields, plus what
|
|
110
|
+
* only the record knows (when it committed, which revision it bound to, how
|
|
111
|
+
* its lines were delivered).
|
|
112
|
+
*
|
|
113
|
+
* Two deliberate differences from `WorldTurnResult`, both because a read-back
|
|
114
|
+
* exists ONLY for a turn that committed:
|
|
115
|
+
*
|
|
116
|
+
* · `completionStatus` / `executionStatus` are the constant `'committed'`,
|
|
117
|
+
* not a member of the live union. A turn that never committed has no entry
|
|
118
|
+
* and answers 404 — there is no status to report;
|
|
119
|
+
* · `deliveryStatus` adds `'unknown'`, which is a real answer once settled
|
|
120
|
+
* rows have been reaped: no queue row is not proof of delivery.
|
|
121
|
+
*
|
|
122
|
+
* The four turn-time facts are optional because rows committed before world
|
|
123
|
+
* API 1.6 do not carry them. Absent means UNKNOWN. */
|
|
124
|
+
export interface WorldTurnReadback {
|
|
125
|
+
turnId: string;
|
|
126
|
+
worldInstanceId: string;
|
|
127
|
+
beforeStateRevision: number;
|
|
128
|
+
afterStateRevision: number;
|
|
129
|
+
roleMessages: Array<{
|
|
130
|
+
roleId: string;
|
|
131
|
+
message: string;
|
|
132
|
+
}>;
|
|
133
|
+
committedStateDiff: unknown[];
|
|
134
|
+
rejectedEffects: Array<{
|
|
135
|
+
index: number;
|
|
136
|
+
reason: string;
|
|
137
|
+
roleId?: string;
|
|
138
|
+
kind?: string;
|
|
139
|
+
}>;
|
|
140
|
+
selectedRoles?: string[];
|
|
141
|
+
skippedRoles?: Array<{
|
|
142
|
+
roleId: string;
|
|
143
|
+
reason: string;
|
|
144
|
+
}>;
|
|
145
|
+
repairs?: Array<{
|
|
146
|
+
roleId: string;
|
|
147
|
+
outcome: 'repaired' | 'fallback';
|
|
148
|
+
reason?: string;
|
|
149
|
+
}>;
|
|
150
|
+
nextOptions?: Array<{
|
|
151
|
+
branchId: string;
|
|
152
|
+
condition: string;
|
|
153
|
+
}>;
|
|
154
|
+
committedAt: number;
|
|
155
|
+
environmentRevision: number;
|
|
156
|
+
storyPackageRef?: {
|
|
157
|
+
packageId: string;
|
|
158
|
+
revision: number;
|
|
159
|
+
contentHash: string;
|
|
160
|
+
};
|
|
161
|
+
eventId?: string;
|
|
162
|
+
traceId?: string;
|
|
163
|
+
completionStatus: 'committed';
|
|
164
|
+
executionStatus: 'committed';
|
|
165
|
+
deliveryStatus: WorldDeliveryStatus | 'unknown';
|
|
166
|
+
deliveryGaps?: unknown[];
|
|
167
|
+
deliveryCounts?: {
|
|
168
|
+
total: number;
|
|
169
|
+
delivered: number;
|
|
170
|
+
[k: string]: number;
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/** One page of the committed timeline. `order` says which way it runs, because
|
|
174
|
+
* the two calls answer different questions: without a cursor you get the
|
|
175
|
+
* newest beats first, with one you get everything after it, oldest first. */
|
|
176
|
+
export interface WorldTimelinePage {
|
|
177
|
+
beats: Array<{
|
|
178
|
+
entryId: string;
|
|
179
|
+
seq: number;
|
|
180
|
+
kind: string;
|
|
181
|
+
actorRoleId: string | null;
|
|
182
|
+
beforeRevision: number;
|
|
183
|
+
afterRevision: number;
|
|
184
|
+
patchCount: number;
|
|
185
|
+
committedAt: number;
|
|
186
|
+
turnId: string | null;
|
|
187
|
+
messages: Array<{
|
|
188
|
+
roleId: string;
|
|
189
|
+
text: string;
|
|
190
|
+
fallback?: true;
|
|
191
|
+
}>;
|
|
192
|
+
effects: Array<{
|
|
193
|
+
kind: string;
|
|
194
|
+
roleId: string;
|
|
195
|
+
target: string | null;
|
|
196
|
+
accepted: boolean;
|
|
197
|
+
code?: WorldRejectionCode;
|
|
198
|
+
reason?: string;
|
|
199
|
+
}>;
|
|
200
|
+
}>;
|
|
201
|
+
order: 'asc' | 'desc';
|
|
202
|
+
/** Present only on a cursor read: pass it back as `sinceSeq` to continue. */
|
|
203
|
+
nextSinceSeq?: number | null;
|
|
204
|
+
}
|
|
109
205
|
export type WorldRehydrateFailure = 'source_missing' | 'archive_unavailable' | 'hash_mismatch' | 'ledger_drift' | 'identity_mismatch';
|
|
110
206
|
/** What to DO about a stuck delivery, in one call. The three answers are the
|
|
111
207
|
* three verbs, in the order they should be tried: re-send what is still there,
|
|
@@ -448,10 +544,38 @@ export declare class PouchyWorldClient {
|
|
|
448
544
|
}>;
|
|
449
545
|
/** Read back a COMMITTED turn. The recovery path when a response was lost:
|
|
450
546
|
* it re-runs nothing, and a 404 means the turn never committed. */
|
|
451
|
-
|
|
547
|
+
/** Read a COMMITTED turn back — the recovery path when a response was lost.
|
|
548
|
+
*
|
|
549
|
+
* As of world API 1.6 this carries the same fields the live result did,
|
|
550
|
+
* `nextOptions` included, so a resumed session can offer the audience the
|
|
551
|
+
* same choices it would have. Rows committed before that carry none of the
|
|
552
|
+
* four turn-time facts: absent means UNKNOWN, never "none". */
|
|
553
|
+
getTurn(environmentId: string, worldInstanceId: string, turnId: string): Promise<WorldTurnReadback>;
|
|
452
554
|
/** Verify that the materialized state is what the committed ledger says.
|
|
453
555
|
* Always a dry run — it reports, it never repairs. Pass the previous
|
|
454
556
|
* report's `cursor` to continue a run that came back `incomplete`. */
|
|
557
|
+
/** ONE page of the committed timeline.
|
|
558
|
+
*
|
|
559
|
+
* Pass `sinceSeq` to resume: you get the beats after it, oldest first, and
|
|
560
|
+
* a `nextSinceSeq` to continue with. Without it you get the newest-first
|
|
561
|
+
* window, which is the "what happened lately" view and cannot be paged
|
|
562
|
+
* past its cap. */
|
|
563
|
+
listTurns(environmentId: string, worldInstanceId: string, opts?: {
|
|
564
|
+
limit?: number;
|
|
565
|
+
sinceSeq?: number;
|
|
566
|
+
}): Promise<WorldTimelinePage>;
|
|
567
|
+
/** Walk the timeline FORWARD from a revision already processed, following
|
|
568
|
+
* the cursor to the end. The resume primitive: a backend that stored the
|
|
569
|
+
* last `seq` it handled calls this once on restart and receives exactly the
|
|
570
|
+
* beats it missed, in order, with no gap at a page boundary and no beat
|
|
571
|
+
* twice.
|
|
572
|
+
*
|
|
573
|
+
* `maxPages` is a stop, not a tuning knob — an unbounded follow loop
|
|
574
|
+
* against a busy world never returns. */
|
|
575
|
+
listTurnsSince(environmentId: string, worldInstanceId: string, sinceSeq: number, opts?: {
|
|
576
|
+
pageSize?: number;
|
|
577
|
+
maxPages?: number;
|
|
578
|
+
}): Promise<WorldTimelinePage['beats']>;
|
|
455
579
|
replayLedger(environmentId: string, worldInstanceId: string, options?: {
|
|
456
580
|
cursor?: string;
|
|
457
581
|
maxEntries?: number;
|
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.9.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
|
|
@@ -275,12 +275,63 @@ export class PouchyWorldClient {
|
|
|
275
275
|
}
|
|
276
276
|
/** Read back a COMMITTED turn. The recovery path when a response was lost:
|
|
277
277
|
* it re-runs nothing, and a 404 means the turn never committed. */
|
|
278
|
+
/** Read a COMMITTED turn back — the recovery path when a response was lost.
|
|
279
|
+
*
|
|
280
|
+
* As of world API 1.6 this carries the same fields the live result did,
|
|
281
|
+
* `nextOptions` included, so a resumed session can offer the audience the
|
|
282
|
+
* same choices it would have. Rows committed before that carry none of the
|
|
283
|
+
* four turn-time facts: absent means UNKNOWN, never "none". */
|
|
278
284
|
getTurn(environmentId, worldInstanceId, turnId) {
|
|
279
285
|
return this.owner('GET', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/turns/${encodeURIComponent(turnId)}`);
|
|
280
286
|
}
|
|
281
287
|
/** Verify that the materialized state is what the committed ledger says.
|
|
282
288
|
* Always a dry run — it reports, it never repairs. Pass the previous
|
|
283
289
|
* report's `cursor` to continue a run that came back `incomplete`. */
|
|
290
|
+
/** ONE page of the committed timeline.
|
|
291
|
+
*
|
|
292
|
+
* Pass `sinceSeq` to resume: you get the beats after it, oldest first, and
|
|
293
|
+
* a `nextSinceSeq` to continue with. Without it you get the newest-first
|
|
294
|
+
* window, which is the "what happened lately" view and cannot be paged
|
|
295
|
+
* past its cap. */
|
|
296
|
+
listTurns(environmentId, worldInstanceId, opts) {
|
|
297
|
+
const q = new URLSearchParams();
|
|
298
|
+
if (opts?.limit !== undefined)
|
|
299
|
+
q.set('limit', String(opts.limit));
|
|
300
|
+
if (opts?.sinceSeq !== undefined)
|
|
301
|
+
q.set('sinceSeq', String(opts.sinceSeq));
|
|
302
|
+
const suffix = q.toString() ? `?${q}` : '';
|
|
303
|
+
return this.owner('GET', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/turns${suffix}`);
|
|
304
|
+
}
|
|
305
|
+
/** Walk the timeline FORWARD from a revision already processed, following
|
|
306
|
+
* the cursor to the end. The resume primitive: a backend that stored the
|
|
307
|
+
* last `seq` it handled calls this once on restart and receives exactly the
|
|
308
|
+
* beats it missed, in order, with no gap at a page boundary and no beat
|
|
309
|
+
* twice.
|
|
310
|
+
*
|
|
311
|
+
* `maxPages` is a stop, not a tuning knob — an unbounded follow loop
|
|
312
|
+
* against a busy world never returns. */
|
|
313
|
+
async listTurnsSince(environmentId, worldInstanceId, sinceSeq, opts) {
|
|
314
|
+
const pageSize = opts?.pageSize ?? 50;
|
|
315
|
+
const maxPages = opts?.maxPages ?? 20;
|
|
316
|
+
const out = [];
|
|
317
|
+
let cursor = sinceSeq;
|
|
318
|
+
for (let page = 0; page < maxPages; page++) {
|
|
319
|
+
const got = await this.listTurns(environmentId, worldInstanceId, {
|
|
320
|
+
limit: pageSize,
|
|
321
|
+
sinceSeq: cursor
|
|
322
|
+
});
|
|
323
|
+
if (!got.beats.length)
|
|
324
|
+
break;
|
|
325
|
+
out.push(...got.beats);
|
|
326
|
+
const next = got.nextSinceSeq;
|
|
327
|
+
// Defensive: a server that stopped advancing the cursor would other-
|
|
328
|
+
// wise spin here re-reading the same page until maxPages.
|
|
329
|
+
if (typeof next !== 'number' || next <= cursor)
|
|
330
|
+
break;
|
|
331
|
+
cursor = next;
|
|
332
|
+
}
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
284
335
|
replayLedger(environmentId, worldInstanceId, options = {}) {
|
|
285
336
|
return this.owner('POST', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/ledger/replay`, options);
|
|
286
337
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pouchy_ai/world-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Server-side TypeScript client for Pouchy World
|
|
3
|
+
"version": "0.9.0",
|
|
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",
|
|
7
7
|
"homepage": "https://pouchy.ai/sdk",
|