@pouchy_ai/world-sdk 0.20.0 → 0.23.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,59 @@
1
1
  # @pouchy_ai/world-sdk
2
2
 
3
+ ## 0.23.0
4
+
5
+ - **`runTurn` gains `focusRoles?: string[]`** — narrow one beat to the named
6
+ roles: an interview, a one-on-one scene, a whispered aside. Before this,
7
+ every caller beat ran (and billed) every bound role up to the server cap,
8
+ and a one-on-one scene was impossible to express through the API.
9
+ - **Shrink-only.** The server intersects the focus with the bound story cast,
10
+ mirroring the event lane's subscription routing: it can never widen a beat
11
+ past the roles the story declares or past the server's own cap. A focus
12
+ that matches no bound role is refused as `no selectable role` and spends
13
+ nothing.
14
+ - Roles outside the focus are **not run and not billed**; the result's
15
+ `skippedRoles` records each as `'not focused'` — a new member of the skip
16
+ vocabulary — so the readback stays honest about who was passed over.
17
+ - Absent or empty means every bound role answers, exactly as before. No
18
+ existing call changes behaviour.
19
+ - Server-side this is world API 1.29.0.
20
+
21
+ ## 0.22.0
22
+
23
+ - **`startNextEpisode(environmentId, worldInstanceId)`** — start the next
24
+ episode of a worldline that just finished one, and receive the CARRYOVER.
25
+ - The worldline does **not** restart. Its `worldInstanceId`, membership, state
26
+ and ledger all continue; the episode moves on. Nothing is copied and nothing
27
+ is deleted, so there is no migration to get wrong.
28
+ - **Idempotent.** The run id is derived from the worldline, the episode and the
29
+ revision it starts at, so a retry lands on the same run and answers
30
+ `created: false` rather than forking the story.
31
+ - **Owner token only** — no `/admin` mirror, same reasoning as `preflightWorld`:
32
+ the mirror carries reads, and this is the write that decides a story moves on.
33
+ - New types: `WorldCarryoverV1`, `WorldNextEpisodeResponse`. The carryover is a
34
+ **projection** of facts the worldline already holds — canon it established,
35
+ where the characters stand, threads left hanging, setups never paid off.
36
+ Role-private notes are never in it, and no memory is carried because the world
37
+ runtime writes none.
38
+ - Server-side this is world API 1.28.0.
39
+
40
+ ## 0.21.0
41
+
42
+ - **`WorldProgressCheckpointV1.episode`** — which episode a serial worldline is
43
+ playing, its beats-spent and turn budget, and the ending once it has one.
44
+ Absent for every world whose pinned story is not a serial, which is most of
45
+ them, so nothing an existing integration reads has changed shape.
46
+ - **Type-only.** No new method, no new route: `getProgress` already returned
47
+ this checkpoint and now returns one more optional field.
48
+ - `beatsCommitted` and `turnBudget` are two numbers, **not a ratio** — the same
49
+ rule as `completedNodeCount` / `declaredNodeCount`. A budget is the ceiling at
50
+ which the author's fallback ending fires, not a denominator of progress.
51
+ - `endingId` is decided **server-side** from committed state and the package's
52
+ declared rules. No client, prompt or model supplies it.
53
+ - Server-side this is world API 1.26.0 — Story Contract v3 (`series`). Publishing
54
+ a v3 package needs `POUCHY_STORY_CONTRACT_V3_WRITE` on the deployment; until
55
+ then such a publish is refused with `code: "story_contract_v3_write_disabled"`.
56
+
3
57
  ## 0.20.0
4
58
 
5
59
  - **Seven methods for routes that already existed.** `getWorldOverview`,
package/README.md CHANGED
@@ -115,6 +115,23 @@ await world.runTurn({
115
115
  no single commit for a deterministic write to ride, and sending patches to one
116
116
  answers 422 rather than dropping them quietly.
117
117
 
118
+ **Focusing a beat on some of the cast.** By default every bound role answers a
119
+ beat (up to the server cap). For an interview or a one-on-one scene, pass
120
+ `focusRoles` and only those roles run — the rest are not billed and are
121
+ recorded in `skippedRoles` as `'not focused'`:
122
+
123
+ ```ts
124
+ const beat = await world.runTurn({
125
+ environmentId, worldInstanceId,
126
+ text: 'Amara, what did you see?',
127
+ focusRoles: ['amara']
128
+ });
129
+ ```
130
+
131
+ Shrink-only: the server intersects the focus with the bound story cast, so it
132
+ can never widen a beat, and a focus matching no bound role is refused as
133
+ `no selectable role` without spending anything.
134
+
118
135
  **Resuming after a crash.** `getTurn` returns the same fields the live result
119
136
  did — `nextOptions` included — so a recovered session can offer the audience
120
137
  the choices it was about to. To catch up on beats you missed entirely, store
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.20.0";
7
+ export declare const WORLD_SDK_VERSION = "0.23.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. */
@@ -216,8 +216,81 @@ export interface WorldProgressCheckpointV1 {
216
216
  branchId: string;
217
217
  condition: string;
218
218
  }>;
219
+ /** The episode this worldline is playing, when its pinned story is a
220
+ * serial. ABSENT for every non-serial world, which is most of them.
221
+ *
222
+ * `beatsCommitted` and `turnBudget` are two numbers, not a ratio — same
223
+ * rule as `completedNodeCount` / `declaredNodeCount`. A budget is the
224
+ * ceiling at which the author's fallback ending fires, not a denominator
225
+ * of progress.
226
+ *
227
+ * `endingId` appears only once the episode is over. It is decided
228
+ * server-side from committed state and the package's declared rules; no
229
+ * client, prompt or model supplies it. */
230
+ episode?: {
231
+ seriesId: string;
232
+ episodeId: string;
233
+ title: string;
234
+ status: 'active' | 'ended';
235
+ beatsCommitted: number;
236
+ turnBudget: number;
237
+ endingId?: string;
238
+ endingTitle?: string;
239
+ };
219
240
  rebuildable: true;
220
241
  }
242
+ /** What the next episode gets to stand on. A PROJECTION of facts the worldline
243
+ * already holds, not a copy of them — nothing is moved and nothing is deleted.
244
+ *
245
+ * Role-private notes are never here. Neither is memory: the world runtime
246
+ * writes none, so there is nothing to inherit. */
247
+ export interface WorldCarryoverV1 {
248
+ contractVersion: 1;
249
+ fromEpisodeId: string;
250
+ fromEndingId: string;
251
+ toEpisodeId: string;
252
+ /** Canon the previous episode established — ids AND the author's text. */
253
+ canonFacts: Array<{
254
+ factId: string;
255
+ text: string;
256
+ }>;
257
+ canonFactsTruncated: boolean;
258
+ /** Where the characters stand. Public by definition. */
259
+ relations: Array<{
260
+ between: [string, string];
261
+ descriptor: string;
262
+ }>;
263
+ relationsTruncated: boolean;
264
+ /** Declared nodes the previous episode never completed. */
265
+ unresolvedThreads: Array<{
266
+ nodeId: string;
267
+ objective: string;
268
+ }>;
269
+ unresolvedThreadsTruncated: boolean;
270
+ /** Declared facts never revealed — setups it did not pay off. Not called
271
+ * "promises": a promise is something a character made, which this system
272
+ * does not model. */
273
+ unfulfilledSetups: Array<{
274
+ factId: string;
275
+ text: string;
276
+ }>;
277
+ unfulfilledSetupsTruncated: boolean;
278
+ }
279
+ export interface WorldNextEpisodeResponse {
280
+ /** False on a retry that landed on the run already started. */
281
+ created: boolean;
282
+ episode: {
283
+ seriesId: string;
284
+ episodeId: string;
285
+ episodeRunId: string;
286
+ status: 'active' | 'ended';
287
+ startedAt: string;
288
+ startedAtRevision: number;
289
+ startedFromEpisodeId?: string;
290
+ startedFromEndingId?: string;
291
+ };
292
+ carryover: WorldCarryoverV1;
293
+ }
221
294
  export interface WorldTurnReadback {
222
295
  turnId: string;
223
296
  worldInstanceId: string;
@@ -949,6 +1022,20 @@ export declare class PouchyWorldClient {
949
1022
  * around: `getWorldOverview` carries `ready` and `blockers` and IS
950
1023
  * mirrored, so a machine has the readiness answer it needs. */
951
1024
  preflightWorld(environmentId: string): Promise<unknown>;
1025
+ /** Start the NEXT episode of a worldline that just finished one.
1026
+ *
1027
+ * The worldline does NOT restart. Its `worldInstanceId`, membership, state
1028
+ * and ledger all continue — that is the point — and this moves the episode
1029
+ * on and hands back the CARRYOVER: what the next episode gets to stand on.
1030
+ *
1031
+ * Idempotent. The run id is derived from the worldline, the episode and the
1032
+ * revision it starts at, so a retried call lands on the same run and
1033
+ * answers `created: false` rather than forking the story.
1034
+ *
1035
+ * OWNER TOKEN ONLY — no `/admin` mirror, on the same reasoning as
1036
+ * `preflightWorld`: the mirror carries reads, and this is the write that
1037
+ * decides a story moves on. */
1038
+ startNextEpisode(environmentId: string, worldInstanceId: string): Promise<WorldNextEpisodeResponse>;
952
1039
  /** Publish the next world revision. Existing world INSTANCES keep the
953
1040
  * revision they were created on — a published change reaches new instances
954
1041
  * only, which is what keeps a running story from changing runtime or rules
@@ -1233,6 +1320,15 @@ export declare class PouchyWorldClient {
1233
1320
  turnId?: string;
1234
1321
  text: string;
1235
1322
  sourceRoleId?: string;
1323
+ /** Narrow this beat to the named roles — an interview, a one-on-one
1324
+ * scene, a whispered aside. Shrink-only: the server intersects it with
1325
+ * the bound story cast, so it can never widen the beat past the roles
1326
+ * the story declares or the server's own cap. Roles outside the focus
1327
+ * are not run and not billed; they appear in the result's
1328
+ * `skippedRoles` as `'not focused'`. A focus that matches no bound
1329
+ * role is refused as "no selectable role" and spends nothing. Absent
1330
+ * or empty means every bound role answers, exactly as before. */
1331
+ focusRoles?: readonly string[];
1236
1332
  traceId?: string;
1237
1333
  /** PROVIDER-authored state changes, committed with this beat.
1238
1334
  *
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.20.0';
26
+ export const WORLD_SDK_VERSION = '0.23.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
@@ -346,6 +346,22 @@ export class PouchyWorldClient {
346
346
  preflightWorld(environmentId) {
347
347
  return this.owner('GET', `/projects/${this.projectId}/environments/${environmentId}/preflight`);
348
348
  }
349
+ /** Start the NEXT episode of a worldline that just finished one.
350
+ *
351
+ * The worldline does NOT restart. Its `worldInstanceId`, membership, state
352
+ * and ledger all continue — that is the point — and this moves the episode
353
+ * on and hands back the CARRYOVER: what the next episode gets to stand on.
354
+ *
355
+ * Idempotent. The run id is derived from the worldline, the episode and the
356
+ * revision it starts at, so a retried call lands on the same run and
357
+ * answers `created: false` rather than forking the story.
358
+ *
359
+ * OWNER TOKEN ONLY — no `/admin` mirror, on the same reasoning as
360
+ * `preflightWorld`: the mirror carries reads, and this is the write that
361
+ * decides a story moves on. */
362
+ startNextEpisode(environmentId, worldInstanceId) {
363
+ return this.owner('POST', `/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/episodes/next`, {});
364
+ }
349
365
  /** Publish the next world revision. Existing world INSTANCES keep the
350
366
  * revision they were created on — a published change reaches new instances
351
367
  * only, which is what keeps a running story from changing runtime or rules
@@ -684,6 +700,7 @@ export class PouchyWorldClient {
684
700
  kind: 'user',
685
701
  text: input.text,
686
702
  ...(input.sourceRoleId ? { sourceRoleId: input.sourceRoleId } : {}),
703
+ ...(input.focusRoles?.length ? { focusRoles: [...input.focusRoles] } : {}),
687
704
  ...(input.proposedPatches ? { proposedPatches: input.proposedPatches } : {})
688
705
  },
689
706
  ...(input.traceId ? { traceId: input.traceId } : {})
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pouchy_ai/world-sdk",
3
- "version": "0.20.0",
3
+ "version": "0.23.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",