@pouchy_ai/world-sdk 0.13.0 → 0.16.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 +67 -0
- package/README.md +27 -2
- package/dist/index.d.ts +218 -1
- package/dist/index.js +102 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
# @pouchy_ai/world-sdk
|
|
2
2
|
|
|
3
|
+
## 0.16.0
|
|
4
|
+
|
|
5
|
+
- `scanConsistency()` — what a world's own record proves about itself. Ten
|
|
6
|
+
deterministic findings, each a comparison between committed facts and the
|
|
7
|
+
pinned story package. Read-only: it commits nothing, asks no model, and
|
|
8
|
+
repairs nothing.
|
|
9
|
+
- **`code` is a closed set that may grow.** A published member is never removed,
|
|
10
|
+
renamed, or widened in meaning; a criterion found to be wrong is deprecated
|
|
11
|
+
and replaced beside it. Handle an unknown code as informational rather than
|
|
12
|
+
failing on it — that is what makes a minor bump safe to take.
|
|
13
|
+
- **Read `coverage` before trusting an empty `findings`.** Some checks read
|
|
14
|
+
fields older ledger rows do not carry, and those entries are counted there
|
|
15
|
+
rather than skipped in silence. A clean report with non-empty coverage is
|
|
16
|
+
clean about less than the whole world.
|
|
17
|
+
- `incomplete` is a status, not an empty result: the walk is capped, a `cursor`
|
|
18
|
+
comes back, and the state- and chain-level checks do not run on a partial
|
|
19
|
+
walk. `entriesScanned` stays honest even once `findingsTruncated` is true.
|
|
20
|
+
- Rate limited on two axes (project and IP, 30/min each, both required).
|
|
21
|
+
`status: 'rate_limited'` arrives with HTTP 429 and `retryAfterSec`. If the
|
|
22
|
+
shared limiter is unreachable the scan still runs — a read-only diagnostic
|
|
23
|
+
should not vanish when a world is being investigated.
|
|
24
|
+
|
|
25
|
+
## 0.15.0
|
|
26
|
+
|
|
27
|
+
- `getProgress()` — where a story is and whether it can go on, as a DERIVED
|
|
28
|
+
checkpoint over facts that already committed. Reads nothing into existence:
|
|
29
|
+
no new collection, no model call, no write.
|
|
30
|
+
- The return is a discriminated union. `status: 'unavailable'` carries **no
|
|
31
|
+
`checkpoint`**, so code that forgets to narrow fails loudly instead of reading
|
|
32
|
+
a plausible zero; `resumable` is typed `false` on that arm.
|
|
33
|
+
- `reason` is coarse on purpose. `world_unavailable` covers "no such instance",
|
|
34
|
+
"no access", "package revoked", "package gone" and "contract too new" as one
|
|
35
|
+
word — telling them apart would let a caller map which objects exist by
|
|
36
|
+
reading refusals. `story_reference_invalid` is distinct and leaks nothing: the
|
|
37
|
+
pinned story and its state disagree.
|
|
38
|
+
- The checkpoint pins its provenance — `environmentRevision`, and the story
|
|
39
|
+
package's exact `revision` + `contentHash`. It follows the INSTANCE's pin, so
|
|
40
|
+
publishing a new story revision never changes what a running world reports.
|
|
41
|
+
- **`declaredNodeCount` is not a denominator.** A branching story never visits
|
|
42
|
+
every declared node; there is no percentage field and `completedNodeCount /
|
|
43
|
+
declaredNodeCount` is not a completion ratio. Show two numbers.
|
|
44
|
+
- **`recentProgressRecords` is not a summary.** `{ seq, kind, at }` says a beat
|
|
45
|
+
happened, never what happened in it. Read `/turns` for the lines.
|
|
46
|
+
- `nextOptions` is the same shape and the same derivation the live turn uses —
|
|
47
|
+
never model-generated, capped server-side.
|
|
48
|
+
- Two reads of an unchanged world are byte-identical: no read-time clock, every
|
|
49
|
+
list explicitly ordered, state and ledger taken in one read-only transaction.
|
|
50
|
+
|
|
51
|
+
## 0.14.0
|
|
52
|
+
|
|
53
|
+
- `deliberate()` and `selectCandidate()` — ask for a few PUBLIC directions a
|
|
54
|
+
beat could take, then commit the one the player chose.
|
|
55
|
+
- Candidates are safe to render straight to a player: `candidateId`, `title`,
|
|
56
|
+
`direction`, `shortTeaser`, `participatingRoles` and nothing else. No
|
|
57
|
+
reasoning, no role secrets, no simulated effects, no scores.
|
|
58
|
+
- `selectCandidate` commits through the ordinary coordinator — same beat, same
|
|
59
|
+
ledger, same `WorldTurnResult`. A deliberation has no privileges over the
|
|
60
|
+
world: its simulated effects are never carried into the real turn.
|
|
61
|
+
- The `envelope` is opaque and belongs on your SERVER. Hand it back unchanged;
|
|
62
|
+
do not send it to a browser. The turn id is fixed inside it, so re-sending the
|
|
63
|
+
same envelope retries rather than committing a second beat.
|
|
64
|
+
- If the world moved since the candidates were produced, `selectCandidate`
|
|
65
|
+
answers 409 `stale_deliberation` having spent nothing — deliberate again.
|
|
66
|
+
- Off unless the world's published revision declares `deliberation`. A world
|
|
67
|
+
without it answers 409 on `deliberate()`, which simply means that story plays
|
|
68
|
+
directly.
|
|
69
|
+
|
|
3
70
|
## 0.13.0
|
|
4
71
|
|
|
5
72
|
- `createWorldSession` now returns a typed `WorldSessionResponse` instead of
|
package/README.md
CHANGED
|
@@ -47,8 +47,12 @@ const world = new PouchyWorldClient({
|
|
|
47
47
|
|
|
48
48
|
**Running** — `createWorldSession`, `runTurn`, `sendEvent`.
|
|
49
49
|
|
|
50
|
-
**
|
|
51
|
-
|
|
50
|
+
**Deliberating** — `deliberate`, `selectCandidate`. Ask for a couple of public
|
|
51
|
+
directions a beat could take, then commit the one the player picked. Off unless
|
|
52
|
+
the world's published revision declares it; the `envelope` is server-side only.
|
|
53
|
+
|
|
54
|
+
**Reading** — `getWorldState`, `getProgress`, `getTurn`, `listTurns`,
|
|
55
|
+
`listTurnsSince`, `replayLedger`, `replayLedgerToEnd`, `scanConsistency`.
|
|
52
56
|
|
|
53
57
|
**Which credential does what.** This matters more than it looks: one of them
|
|
54
58
|
expires within the hour.
|
|
@@ -109,6 +113,27 @@ Both are additive: a turn committed before world API 1.6 carries none of the
|
|
|
109
113
|
four turn-time facts (`selectedRoles`, `skippedRoles`, `repairs`,
|
|
110
114
|
`nextOptions`). Absent means UNKNOWN, never "none".
|
|
111
115
|
|
|
116
|
+
**Reopening a story.** `getProgress` answers the question a returning reader's
|
|
117
|
+
client has — where was I, and can I go on? — without replaying anything.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const p = await world.getProgress(envId, instanceId);
|
|
121
|
+
if (p.status !== 'ready') return renderStartFresh(); // narrow FIRST
|
|
122
|
+
renderScene(p.checkpoint.currentScene); // may be null
|
|
123
|
+
renderCounts(p.checkpoint.completedNodeCount, p.checkpoint.declaredNodeCount);
|
|
124
|
+
renderDirections(p.checkpoint.nextOptions);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Narrowing on `status` is not politeness: the `unavailable` arm has no
|
|
128
|
+
`checkpoint` key, so skipping the check reads `undefined` rather than telling a
|
|
129
|
+
reader they never started a story they are halfway through.
|
|
130
|
+
|
|
131
|
+
Two shapes it does not have. **`declaredNodeCount` is not a denominator** — a
|
|
132
|
+
branching story never visits every node the author declared, so show the two
|
|
133
|
+
counts, never a percentage. And **`recentProgressRecords` is not a summary**:
|
|
134
|
+
`{ seq, kind, at }` says a beat happened, not what happened in it; the lines
|
|
135
|
+
live behind `getTurn` / `listTurns`.
|
|
136
|
+
|
|
112
137
|
**Content return** — `createScriptDraft`, `getScriptDraft`, `listScriptDrafts`,
|
|
113
138
|
`reviewScriptDraft`, `exportScriptDraft`.
|
|
114
139
|
|
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,33 @@
|
|
|
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.
|
|
7
|
+
export declare const WORLD_SDK_VERSION = "0.16.0";
|
|
8
8
|
export declare const DEFAULT_BASE_URL = "https://pouchy.ai/v1";
|
|
9
|
+
/** One direction a beat could take. The WHOLE of what a deliberation shows a
|
|
10
|
+
* player: no reasoning, no role secrets, no simulated effects, no scores. */
|
|
11
|
+
export interface WorldDeliberationCandidate {
|
|
12
|
+
candidateId: string;
|
|
13
|
+
title: string;
|
|
14
|
+
/** What the scene will be steered toward. Public, because the player is
|
|
15
|
+
* choosing it and must be able to read what they are choosing. */
|
|
16
|
+
direction: string;
|
|
17
|
+
shortTeaser: string;
|
|
18
|
+
participatingRoles: string[];
|
|
19
|
+
}
|
|
20
|
+
export interface WorldDeliberationResponse {
|
|
21
|
+
candidates: WorldDeliberationCandidate[];
|
|
22
|
+
/** Signed evidence. Opaque — hand it back to `selectCandidate` unchanged. */
|
|
23
|
+
envelope: string;
|
|
24
|
+
expiresAt: number;
|
|
25
|
+
budgetSpent: {
|
|
26
|
+
modelCalls: number;
|
|
27
|
+
tokens: number;
|
|
28
|
+
ms: number;
|
|
29
|
+
};
|
|
30
|
+
/** Why it stopped. A run that hit a ceiling says so rather than letting a
|
|
31
|
+
* short list pass for a complete one. */
|
|
32
|
+
stop: 'complete' | 'candidate_cap' | 'model_call_cap' | 'token_cap' | 'time_cap' | 'no_roles';
|
|
33
|
+
}
|
|
9
34
|
/** The refusal classes a world call can produce. `unknown` is deliberate: an
|
|
10
35
|
* unrecognized status is never quietly folded into a neighbour. */
|
|
11
36
|
export declare const WORLD_ERROR_CODES: readonly ["unauthorized", "forbidden", "not_found", "conflict", "unprocessable", "rate_limited", "payload_too_large", "server_error", "network", "unknown"];
|
|
@@ -131,6 +156,68 @@ export interface WorldTurnResult {
|
|
|
131
156
|
*
|
|
132
157
|
* The four turn-time facts are optional because rows committed before world
|
|
133
158
|
* API 1.6 do not carry them. Absent means UNKNOWN. */
|
|
159
|
+
/** Where a story is, and whether it can go on. World API 1.18.
|
|
160
|
+
*
|
|
161
|
+
* A DISCRIMINATED UNION, not an optional-field bag. The `unavailable` arm has
|
|
162
|
+
* no `checkpoint` key at all, so narrowing on `status` is not a style choice:
|
|
163
|
+
* it is the only way to reach the data, and code that skips it fails loudly
|
|
164
|
+
* instead of reading a plausible zero. `resumable` is typed `false` on that
|
|
165
|
+
* arm, so "unavailable but resumable" cannot be constructed. */
|
|
166
|
+
export type WorldProgressResponse = {
|
|
167
|
+
status: 'ready';
|
|
168
|
+
resumable: boolean;
|
|
169
|
+
checkpoint: WorldProgressCheckpointV1;
|
|
170
|
+
} | {
|
|
171
|
+
status: 'unavailable';
|
|
172
|
+
resumable: false;
|
|
173
|
+
reason: WorldProgressUnavailableReason;
|
|
174
|
+
};
|
|
175
|
+
/** `world_unavailable` is deliberately coarse — no such instance, no access, a
|
|
176
|
+
* revoked or deleted package, a contract too new to read. Distinguishing them
|
|
177
|
+
* would let a caller map which internal objects exist by reading refusals.
|
|
178
|
+
* `story_reference_invalid` is different in kind: the pinned story and the
|
|
179
|
+
* state it is pinned to disagree, which reveals nothing about existence. */
|
|
180
|
+
export type WorldProgressUnavailableReason = 'world_unavailable' | 'story_reference_invalid';
|
|
181
|
+
export interface WorldProgressCheckpointV1 {
|
|
182
|
+
contractVersion: 1;
|
|
183
|
+
/** The revision this INSTANCE is pinned to for life. */
|
|
184
|
+
environmentRevision: number;
|
|
185
|
+
storyPackageRef: {
|
|
186
|
+
packageId: string;
|
|
187
|
+
revision: number;
|
|
188
|
+
contentHash: string;
|
|
189
|
+
};
|
|
190
|
+
stateRevision: number;
|
|
191
|
+
started: boolean;
|
|
192
|
+
/** Null when no scene has been opened. Never a substitute for one the
|
|
193
|
+
* pinned story does not declare — that answers `story_reference_invalid`. */
|
|
194
|
+
currentScene: {
|
|
195
|
+
sceneId: string;
|
|
196
|
+
title: string;
|
|
197
|
+
} | null;
|
|
198
|
+
completedNodeCount: number;
|
|
199
|
+
/** What the AUTHOR declared. NOT a denominator: a branching story never
|
|
200
|
+
* visits every node, so `completedNodeCount / declaredNodeCount` is not a
|
|
201
|
+
* completion ratio. Render them as two numbers. */
|
|
202
|
+
declaredNodeCount: number;
|
|
203
|
+
publicRelations: Array<{
|
|
204
|
+
between: [string, string];
|
|
205
|
+
descriptor: string;
|
|
206
|
+
}>;
|
|
207
|
+
publicRelationsTruncated: boolean;
|
|
208
|
+
/** Newest first. That a beat happened — never what happened in it. */
|
|
209
|
+
recentProgressRecords: Array<{
|
|
210
|
+
seq: number;
|
|
211
|
+
kind: 'turn' | 'event' | 'system';
|
|
212
|
+
at: number;
|
|
213
|
+
}>;
|
|
214
|
+
recentProgressTruncated: boolean;
|
|
215
|
+
nextOptions: Array<{
|
|
216
|
+
branchId: string;
|
|
217
|
+
condition: string;
|
|
218
|
+
}>;
|
|
219
|
+
rebuildable: true;
|
|
220
|
+
}
|
|
134
221
|
export interface WorldTurnReadback {
|
|
135
222
|
turnId: string;
|
|
136
223
|
worldInstanceId: string;
|
|
@@ -554,6 +641,65 @@ export interface WorldClientOptions {
|
|
|
554
641
|
* default is generous on purpose, and lower is usually wrong. */
|
|
555
642
|
timeoutMs?: number;
|
|
556
643
|
}
|
|
644
|
+
/** What a world's own record proves about itself. World API 1.19.
|
|
645
|
+
*
|
|
646
|
+
* DETERMINISTIC ONLY. Every finding is a comparison between committed facts
|
|
647
|
+
* and the story package the instance pins — never a judgement about writing.
|
|
648
|
+
* Nothing here repairs anything, and nothing here is a suggestion.
|
|
649
|
+
*
|
|
650
|
+
* `code` is a CLOSED set that may GROW. A published member is never removed,
|
|
651
|
+
* renamed, or widened in meaning; a criterion later found to be wrong is
|
|
652
|
+
* deprecated and replaced beside it. So handle an unknown code as
|
|
653
|
+
* informational rather than failing on it — that is what makes a minor bump
|
|
654
|
+
* safe to take. */
|
|
655
|
+
export type WorldConsistencyFindingCode = 'revision_mismatch' | 'ledger_drift' | 'ledger_missing_entries' | 'unrecorded_beat_claim' | 'orphan_branch_reference' | 'node_prereq_violation' | 'effect_without_grant' | 'flag_schema_violation' | 'relation_change_without_cause' | 'verbatim_secret_disclosure';
|
|
656
|
+
export interface WorldConsistencyEvidence {
|
|
657
|
+
turnId?: string;
|
|
658
|
+
seq?: number;
|
|
659
|
+
roleId?: string;
|
|
660
|
+
sceneId?: string;
|
|
661
|
+
nodeId?: string;
|
|
662
|
+
branchId?: string;
|
|
663
|
+
flagKey?: string;
|
|
664
|
+
effectKind?: string;
|
|
665
|
+
/** Only on verbatim matches, and only the matched fragment — never a line. */
|
|
666
|
+
quote?: string;
|
|
667
|
+
/** Passed through from a committed rejection AS EVIDENCE. It is not this
|
|
668
|
+
* finding's classification; the two vocabularies stay separate. */
|
|
669
|
+
ledgerRejectionCode?: string;
|
|
670
|
+
}
|
|
671
|
+
export interface WorldConsistencyFinding {
|
|
672
|
+
code: WorldConsistencyFindingCode;
|
|
673
|
+
layer: 'deterministic';
|
|
674
|
+
status: 'proven';
|
|
675
|
+
severity: 'blocking' | 'warning' | 'info';
|
|
676
|
+
/** Never empty. A finding nobody can check is an accusation. */
|
|
677
|
+
evidence: WorldConsistencyEvidence[];
|
|
678
|
+
}
|
|
679
|
+
/** What a check could NOT examine. Read this before trusting an empty
|
|
680
|
+
* `findings`: a clean report with non-empty coverage is clean about less than
|
|
681
|
+
* the whole world. Absence of a finding is not proof of absence. */
|
|
682
|
+
export interface WorldConsistencyCoverage {
|
|
683
|
+
code: WorldConsistencyFindingCode;
|
|
684
|
+
skipped: number;
|
|
685
|
+
reason: 'evidence_field_absent' | 'scan_incomplete';
|
|
686
|
+
}
|
|
687
|
+
export type WorldConsistencyReport = {
|
|
688
|
+
status: 'complete' | 'incomplete';
|
|
689
|
+
contractVersion: number;
|
|
690
|
+
findings: WorldConsistencyFinding[];
|
|
691
|
+
coverage: WorldConsistencyCoverage[];
|
|
692
|
+
entriesScanned: number;
|
|
693
|
+
findingsTruncated: boolean;
|
|
694
|
+
/** Present only when `status` is `incomplete`. Pass it back to continue. */
|
|
695
|
+
cursor?: number;
|
|
696
|
+
} | {
|
|
697
|
+
status: 'rate_limited';
|
|
698
|
+
retryAfterSec: number;
|
|
699
|
+
} | {
|
|
700
|
+
status: 'unavailable';
|
|
701
|
+
reason: 'world_unavailable';
|
|
702
|
+
};
|
|
557
703
|
export declare class PouchyWorldClient {
|
|
558
704
|
readonly projectId: string;
|
|
559
705
|
private readonly baseUrl;
|
|
@@ -605,6 +751,48 @@ export declare class PouchyWorldClient {
|
|
|
605
751
|
environmentRevision: number;
|
|
606
752
|
state: Record<string, unknown>;
|
|
607
753
|
}>;
|
|
754
|
+
/** What this world's own record proves about itself. World API 1.19.
|
|
755
|
+
*
|
|
756
|
+
* A read-only scan: it commits nothing, asks no model, and repairs nothing.
|
|
757
|
+
* Findings are deterministic — each one is two recorded things that cannot
|
|
758
|
+
* both be true — so they are safe to act on without a human re-reading the
|
|
759
|
+
* story, which is exactly what the model-assisted layer is not.
|
|
760
|
+
*
|
|
761
|
+
* Check `status` first. `incomplete` means the walk was capped or the
|
|
762
|
+
* datastore stopped answering: pass `cursor` back to continue, and do NOT
|
|
763
|
+
* read an empty `findings` on that arm as a clean bill of health.
|
|
764
|
+
*
|
|
765
|
+
* READ `coverage` BEFORE TRUSTING AN EMPTY `findings`. Some checks depend
|
|
766
|
+
* on fields that older ledger rows do not carry, and those entries are
|
|
767
|
+
* counted there rather than skipped in silence. A clean report that could
|
|
768
|
+
* not examine half the history is not the same as a clean world.
|
|
769
|
+
*
|
|
770
|
+
* Rate limited on two axes, project and IP. `status: 'rate_limited'`
|
|
771
|
+
* arrives with HTTP 429 and `retryAfterSec`.
|
|
772
|
+
*/
|
|
773
|
+
scanConsistency(environmentId: string, worldInstanceId: string, opts?: {
|
|
774
|
+
cursor?: number;
|
|
775
|
+
}): Promise<WorldConsistencyReport>;
|
|
776
|
+
/** Where the story is, and whether it can go on. World API 1.18.
|
|
777
|
+
*
|
|
778
|
+
* A DERIVED checkpoint: nothing is stored to answer it, no model is asked,
|
|
779
|
+
* and the story it reports is the package this INSTANCE pinned, at its
|
|
780
|
+
* exact revision and content hash — never whatever was published since.
|
|
781
|
+
*
|
|
782
|
+
* Check `status` before reading anything else. `unavailable` has no
|
|
783
|
+
* `checkpoint` at all, which is deliberate: a caller that skips the check
|
|
784
|
+
* gets `undefined` rather than a plausible zero. Its `reason` is
|
|
785
|
+
* intentionally coarse — one word covers "no such instance", "no access",
|
|
786
|
+
* "package revoked", "package gone" and "contract too new", because telling
|
|
787
|
+
* them apart would let a caller map which objects exist by reading errors.
|
|
788
|
+
*
|
|
789
|
+
* Two things it does NOT give you. There is no completion percentage:
|
|
790
|
+
* `declaredNodeCount` is what the AUTHOR wrote, and a branching story never
|
|
791
|
+
* visits all of it, so a ratio of the two counts is meaningless — show them
|
|
792
|
+
* as two numbers. And `recentProgressRecords` says a beat happened, not
|
|
793
|
+
* what happened in it; for the lines themselves, read the timeline.
|
|
794
|
+
*/
|
|
795
|
+
getProgress(environmentId: string, worldInstanceId: string): Promise<WorldProgressResponse>;
|
|
608
796
|
/** Read back a COMMITTED turn. The recovery path when a response was lost:
|
|
609
797
|
* it re-runs nothing, and a 404 means the turn never committed. */
|
|
610
798
|
/** Read a COMMITTED turn back — the recovery path when a response was lost.
|
|
@@ -811,6 +999,35 @@ export declare class PouchyWorldClient {
|
|
|
811
999
|
* role's private notes. */
|
|
812
1000
|
proposedPatches?: readonly Record<string, unknown>[];
|
|
813
1001
|
}): Promise<WorldTurnResult>;
|
|
1002
|
+
/** Ask for a few PUBLIC directions this beat could take, without taking it.
|
|
1003
|
+
*
|
|
1004
|
+
* Off unless the world's published revision declares `deliberation`. Nothing
|
|
1005
|
+
* is written and no candidate is stored: what comes back is a short list
|
|
1006
|
+
* plus a signed `envelope` you hand to `selectCandidate`. The candidates are
|
|
1007
|
+
* public by construction — no reasoning, no role secrets, no simulated
|
|
1008
|
+
* effects — so they are safe to render straight to a player. */
|
|
1009
|
+
deliberate(input: {
|
|
1010
|
+
environmentId: string;
|
|
1011
|
+
worldInstanceId: string;
|
|
1012
|
+
requestId?: string;
|
|
1013
|
+
text: string;
|
|
1014
|
+
model?: string;
|
|
1015
|
+
}): Promise<WorldDeliberationResponse>;
|
|
1016
|
+
/** Commit the direction the player chose. This IS an ordinary beat — the
|
|
1017
|
+
* same coordinator, the same ledger, the same result shape.
|
|
1018
|
+
*
|
|
1019
|
+
* Re-send the same `envelope` to retry: the turn id is fixed inside it, so a
|
|
1020
|
+
* second call is a duplicate rather than a second beat. If the world moved
|
|
1021
|
+
* since the candidates were produced, this answers 409 `stale_deliberation`
|
|
1022
|
+
* and spends nothing — deliberate again. */
|
|
1023
|
+
selectCandidate(input: {
|
|
1024
|
+
environmentId: string;
|
|
1025
|
+
worldInstanceId: string;
|
|
1026
|
+
envelope: string;
|
|
1027
|
+
candidateId: string;
|
|
1028
|
+
direction: string;
|
|
1029
|
+
text: string;
|
|
1030
|
+
}): Promise<WorldTurnResult>;
|
|
814
1031
|
/** Send a trusted EVENT into a world. On a `coordinated` world this becomes
|
|
815
1032
|
* one coordinator turn; on an `actor` world it wakes each subscribed role.
|
|
816
1033
|
* Either way `eventId` is the dedupe key — re-send it freely. */
|
package/dist/index.js
CHANGED
|
@@ -23,9 +23,24 @@ 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.
|
|
26
|
+
export const WORLD_SDK_VERSION = '0.16.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
|
+
* the server will commit under. The payload half is base64url JSON; the
|
|
30
|
+
* signature is not needed to read it, and reading it proves nothing — the
|
|
31
|
+
* SERVER verifies. This is only so the client signs the right slot. */
|
|
32
|
+
function commitTurnIdOf(envelope) {
|
|
33
|
+
const payload = envelope.slice(0, envelope.lastIndexOf('.'));
|
|
34
|
+
try {
|
|
35
|
+
const json = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
36
|
+
if (typeof json.commitTurnId === 'string' && json.commitTurnId)
|
|
37
|
+
return json.commitTurnId;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
/* fall through */
|
|
41
|
+
}
|
|
42
|
+
throw new Error('deliberation envelope is malformed — re-run deliberate()');
|
|
43
|
+
}
|
|
29
44
|
/** The refusal classes a world call can produce. `unknown` is deliberate: an
|
|
30
45
|
* unrecognized status is never quietly folded into a neighbour. */
|
|
31
46
|
export const WORLD_ERROR_CODES = [
|
|
@@ -282,6 +297,58 @@ export class PouchyWorldClient {
|
|
|
282
297
|
getWorldState(environmentId, worldInstanceId) {
|
|
283
298
|
return this.read(`/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/state`, `/admin/environments/${environmentId}/instances/${worldInstanceId}/state`);
|
|
284
299
|
}
|
|
300
|
+
/** What this world's own record proves about itself. World API 1.19.
|
|
301
|
+
*
|
|
302
|
+
* A read-only scan: it commits nothing, asks no model, and repairs nothing.
|
|
303
|
+
* Findings are deterministic — each one is two recorded things that cannot
|
|
304
|
+
* both be true — so they are safe to act on without a human re-reading the
|
|
305
|
+
* story, which is exactly what the model-assisted layer is not.
|
|
306
|
+
*
|
|
307
|
+
* Check `status` first. `incomplete` means the walk was capped or the
|
|
308
|
+
* datastore stopped answering: pass `cursor` back to continue, and do NOT
|
|
309
|
+
* read an empty `findings` on that arm as a clean bill of health.
|
|
310
|
+
*
|
|
311
|
+
* READ `coverage` BEFORE TRUSTING AN EMPTY `findings`. Some checks depend
|
|
312
|
+
* on fields that older ledger rows do not carry, and those entries are
|
|
313
|
+
* counted there rather than skipped in silence. A clean report that could
|
|
314
|
+
* not examine half the history is not the same as a clean world.
|
|
315
|
+
*
|
|
316
|
+
* Rate limited on two axes, project and IP. `status: 'rate_limited'`
|
|
317
|
+
* arrives with HTTP 429 and `retryAfterSec`.
|
|
318
|
+
*/
|
|
319
|
+
scanConsistency(environmentId, worldInstanceId, opts = {}) {
|
|
320
|
+
// The query rides the third argument, not the path template. Both are
|
|
321
|
+
// sent identically; keeping the path a pure template is what lets the
|
|
322
|
+
// admin-lane gate check that every `/admin/...` the SDK sends is a route
|
|
323
|
+
// that actually exists.
|
|
324
|
+
const q = new URLSearchParams();
|
|
325
|
+
if (opts.cursor !== undefined)
|
|
326
|
+
q.set('cursor', String(opts.cursor));
|
|
327
|
+
const suffix = q.toString() ? `?${q}` : '';
|
|
328
|
+
return this.read(`/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/consistency`, `/admin/environments/${environmentId}/instances/${worldInstanceId}/consistency`, suffix);
|
|
329
|
+
}
|
|
330
|
+
/** Where the story is, and whether it can go on. World API 1.18.
|
|
331
|
+
*
|
|
332
|
+
* A DERIVED checkpoint: nothing is stored to answer it, no model is asked,
|
|
333
|
+
* and the story it reports is the package this INSTANCE pinned, at its
|
|
334
|
+
* exact revision and content hash — never whatever was published since.
|
|
335
|
+
*
|
|
336
|
+
* Check `status` before reading anything else. `unavailable` has no
|
|
337
|
+
* `checkpoint` at all, which is deliberate: a caller that skips the check
|
|
338
|
+
* gets `undefined` rather than a plausible zero. Its `reason` is
|
|
339
|
+
* intentionally coarse — one word covers "no such instance", "no access",
|
|
340
|
+
* "package revoked", "package gone" and "contract too new", because telling
|
|
341
|
+
* them apart would let a caller map which objects exist by reading errors.
|
|
342
|
+
*
|
|
343
|
+
* Two things it does NOT give you. There is no completion percentage:
|
|
344
|
+
* `declaredNodeCount` is what the AUTHOR wrote, and a branching story never
|
|
345
|
+
* visits all of it, so a ratio of the two counts is meaningless — show them
|
|
346
|
+
* as two numbers. And `recentProgressRecords` says a beat happened, not
|
|
347
|
+
* what happened in it; for the lines themselves, read the timeline.
|
|
348
|
+
*/
|
|
349
|
+
getProgress(environmentId, worldInstanceId) {
|
|
350
|
+
return this.read(`/projects/${this.projectId}/environments/${environmentId}/instances/${worldInstanceId}/progress`, `/admin/environments/${environmentId}/instances/${worldInstanceId}/progress`);
|
|
351
|
+
}
|
|
285
352
|
/** Read back a COMMITTED turn. The recovery path when a response was lost:
|
|
286
353
|
* it re-runs nothing, and a 404 means the turn never committed. */
|
|
287
354
|
/** Read a COMMITTED turn back — the recovery path when a response was lost.
|
|
@@ -521,6 +588,39 @@ export class PouchyWorldClient {
|
|
|
521
588
|
};
|
|
522
589
|
return this.signed(`/projects/${this.projectId}/environments/${input.environmentId}/instances/${input.worldInstanceId}/turns`, body, turnId);
|
|
523
590
|
}
|
|
591
|
+
/** Ask for a few PUBLIC directions this beat could take, without taking it.
|
|
592
|
+
*
|
|
593
|
+
* Off unless the world's published revision declares `deliberation`. Nothing
|
|
594
|
+
* is written and no candidate is stored: what comes back is a short list
|
|
595
|
+
* plus a signed `envelope` you hand to `selectCandidate`. The candidates are
|
|
596
|
+
* public by construction — no reasoning, no role secrets, no simulated
|
|
597
|
+
* effects — so they are safe to render straight to a player. */
|
|
598
|
+
deliberate(input) {
|
|
599
|
+
const requestId = input.requestId ?? newTurnId('dlb-req');
|
|
600
|
+
const body = { requestId, text: input.text, ...(input.model ? { model: input.model } : {}) };
|
|
601
|
+
return this.signed(`/projects/${this.projectId}/environments/${input.environmentId}/instances/${input.worldInstanceId}/deliberations`, body,
|
|
602
|
+
// The signature's id slot is namespaced, so a deliberation signature
|
|
603
|
+
// can never be replayed onto the turn door.
|
|
604
|
+
`dlb:${requestId}`);
|
|
605
|
+
}
|
|
606
|
+
/** Commit the direction the player chose. This IS an ordinary beat — the
|
|
607
|
+
* same coordinator, the same ledger, the same result shape.
|
|
608
|
+
*
|
|
609
|
+
* Re-send the same `envelope` to retry: the turn id is fixed inside it, so a
|
|
610
|
+
* second call is a duplicate rather than a second beat. If the world moved
|
|
611
|
+
* since the candidates were produced, this answers 409 `stale_deliberation`
|
|
612
|
+
* and spends nothing — deliberate again. */
|
|
613
|
+
selectCandidate(input) {
|
|
614
|
+
const body = {
|
|
615
|
+
envelope: input.envelope,
|
|
616
|
+
candidateId: input.candidateId,
|
|
617
|
+
direction: input.direction,
|
|
618
|
+
text: input.text
|
|
619
|
+
};
|
|
620
|
+
// The id slot is the turn id the envelope already fixed, so the signature
|
|
621
|
+
// covers the id the server will actually commit under.
|
|
622
|
+
return this.signed(`/projects/${this.projectId}/environments/${input.environmentId}/instances/${input.worldInstanceId}/deliberations/select`, body, commitTurnIdOf(input.envelope));
|
|
623
|
+
}
|
|
524
624
|
/** Send a trusted EVENT into a world. On a `coordinated` world this becomes
|
|
525
625
|
* one coordinator turn; on an `actor` world it wakes each subscribed role.
|
|
526
626
|
* Either way `eventId` is the dedupe key — re-send it freely. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pouchy_ai/world-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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",
|