castle-web-sdk 0.4.34 → 0.4.36
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/README.md +201 -7
- package/dist/agent.d.ts +6 -0
- package/dist/agent.js +70 -0
- package/dist/castle.d.ts +5 -0
- package/dist/castle.js +2 -0
- package/dist/commands.d.ts +154 -0
- package/dist/commands.js +46 -0
- package/dist/decks.d.ts +7 -0
- package/dist/decks.js +31 -0
- package/dist/multiplayer.d.ts +3 -0
- package/dist/multiplayer.js +13 -0
- package/dist/multiplayerVoice.d.ts +67 -0
- package/dist/multiplayerVoice.js +309 -0
- package/dist/runtime.d.ts +4 -2
- package/dist/runtime.js +51 -40
- package/dist/server/storage.d.ts +44 -0
- package/dist/server/storage.js +54 -0
- package/dist/transport.d.ts +7 -2
- package/dist/transport.js +120 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,7 +24,10 @@ import { setup, initCard, Store, Leaderboard } from "castle-web-sdk";
|
|
|
24
24
|
- [Pass](#pass)
|
|
25
25
|
- [Snapshot](#snapshot)
|
|
26
26
|
- [Portal](#portal)
|
|
27
|
+
- [Decks](#decks)
|
|
28
|
+
- [Clipboard](#clipboard)
|
|
27
29
|
- [Haptics](#haptics)
|
|
30
|
+
- [Agent](#agent)
|
|
28
31
|
- [Lifecycle](#lifecycle)
|
|
29
32
|
- [Multiplayer](#multiplayer)
|
|
30
33
|
- [Setup](#setup)
|
|
@@ -249,11 +252,30 @@ guide.
|
|
|
249
252
|
|
|
250
253
|
### Limits
|
|
251
254
|
|
|
252
|
-
The platform caps key length, value size, how many keys one
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
255
|
+
The platform caps key length, value size, how many keys one read may
|
|
256
|
+
name, and how much a deck may store in total, and it limits how fast a
|
|
257
|
+
player may read and write. Going over one of these rejects with a
|
|
258
|
+
`CastleError` whose `code` says which, so a deck can tell a player it is
|
|
259
|
+
saving too fast apart from a deck that is out of space:
|
|
260
|
+
|
|
261
|
+
| limit | value |
|
|
262
|
+
|---|---|
|
|
263
|
+
| key | 128 bytes |
|
|
264
|
+
| value | 256 KB |
|
|
265
|
+
| keys in one `get` | 32 |
|
|
266
|
+
| whole deck | 50 MB, 500,000 keys |
|
|
267
|
+
| one player's calls | 20 a second, 512 KB a second |
|
|
268
|
+
| every player of a deck together | 500 calls a second, 8 MB a second |
|
|
269
|
+
|
|
270
|
+
These do not reject. `set` and `remove` with more than 32 keys are split
|
|
271
|
+
into several calls by the SDK (each still counts against the rate above).
|
|
272
|
+
A `list` page holds at most 1,000 entries (a larger `limit` is lowered to
|
|
273
|
+
it) and ends early once it reaches 1 MB; take the next page with its
|
|
274
|
+
`cursor`.
|
|
275
|
+
|
|
276
|
+
A value is JSON, so binary data such as a picture or a video clip costs
|
|
277
|
+
about a third more once encoded, and a clip of more than a few seconds
|
|
278
|
+
does not fit in one. Ship media with the deck instead.
|
|
257
279
|
|
|
258
280
|
## Storage
|
|
259
281
|
|
|
@@ -265,6 +287,10 @@ player it is saving too fast apart from a deck that is out of space.
|
|
|
265
287
|
|
|
266
288
|
`Storage` saves data for the current player. Nobody else can read it.
|
|
267
289
|
|
|
290
|
+
A player's whole `Storage` document is at most 2 MB. A write that takes
|
|
291
|
+
it past that never saves, and nothing tells the deck: `set` has already
|
|
292
|
+
returned.
|
|
293
|
+
|
|
268
294
|
- **`Storage.get<T>(key): Promise<T | null>`** — the value at `key`, or
|
|
269
295
|
`null` if it is not set.
|
|
270
296
|
- **`Storage.set(key, value)`** — sets `key`. `value` must be something
|
|
@@ -293,7 +319,9 @@ await Store.private.set({ level: 7 });
|
|
|
293
319
|
> and adds counters, listing, and leaderboards.
|
|
294
320
|
|
|
295
321
|
`SharedStorage` saves data that other players can read. Values must be
|
|
296
|
-
something that converts to JSON, same as `Storage`.
|
|
322
|
+
something that converts to JSON, same as `Storage`. Each bucket holds at
|
|
323
|
+
most 2 MB, and a write past that is dropped without an error, as with
|
|
324
|
+
`Storage`.
|
|
297
325
|
|
|
298
326
|
Scopes: `'deck'` is one bucket for the whole deck, readable and writable
|
|
299
327
|
by any player. `'user'` is a per-player bucket that anyone can read and
|
|
@@ -520,6 +548,9 @@ when the player collects or closes it. `options`:
|
|
|
520
548
|
|
|
521
549
|
Collecting works only in the Castle mobile app; the website shows "collect in
|
|
522
550
|
the app" and the dev server a short notice, and both resolve `unavailable`.
|
|
551
|
+
`offer` was added to the app on 2026-09-22; an app build from before then
|
|
552
|
+
rejects it with `UNKNOWN_COMMAND` (see [CastleError](#castleerror)), so catch
|
|
553
|
+
it where a deck has to keep going without the card.
|
|
523
554
|
`SnapshotOfferResult` has a `status`:
|
|
524
555
|
|
|
525
556
|
- `'collected'` — the player collected it.
|
|
@@ -534,6 +565,11 @@ const { status } = await Snapshot.offer({
|
|
|
534
565
|
});
|
|
535
566
|
```
|
|
536
567
|
|
|
568
|
+
A snapshot is not the deck's cover, and no SDK call sets the cover. A cover
|
|
569
|
+
the creator chose by hand is kept; otherwise `save-deck` photographs the deck
|
|
570
|
+
as it opens, falling back to a `preview.png` the deck folder holds when that
|
|
571
|
+
capture does not produce one.
|
|
572
|
+
|
|
537
573
|
### `Snapshot.onTaken(handler): () => void`
|
|
538
574
|
|
|
539
575
|
Registers a function called just before an offer's capture, the twin of the
|
|
@@ -604,7 +640,9 @@ label as the fallback. `PortalInfoResult` has a `status`:
|
|
|
604
640
|
`null`), and `coverUrl`, a `data:` url of the deck's cover or `null`.
|
|
605
641
|
- `'broken'` — the portal wouldn't work for this player: the deck doesn't
|
|
606
642
|
exist, has portals turned off, or isn't visible to them.
|
|
607
|
-
- `'unavailable'` — the host couldn't look it up.
|
|
643
|
+
- `'unavailable'` — the host couldn't look it up. This includes a Castle
|
|
644
|
+
app built before `portal.info` was added (2026-09-22): `info` never
|
|
645
|
+
throws `UNKNOWN_COMMAND`.
|
|
608
646
|
|
|
609
647
|
```js
|
|
610
648
|
const info = await Portal.info(doorway.deckId);
|
|
@@ -616,6 +654,45 @@ if (info.status === "ok") {
|
|
|
616
654
|
|
|
617
655
|
Answers are kept per deck for the session, so asking again is cheap.
|
|
618
656
|
|
|
657
|
+
## Decks
|
|
658
|
+
|
|
659
|
+
`Decks` lists decks the player can point a portal at: their own published
|
|
660
|
+
decks, and the decks they saved. A deck can offer these as a picker rather
|
|
661
|
+
than asking for a pasted link.
|
|
662
|
+
Decks is limited to Castle admins for now; for every other player both calls
|
|
663
|
+
reject with a `PERMISSION_DENIED` error.
|
|
664
|
+
|
|
665
|
+
### `Decks.mine(options?): Promise<DecksListResult>`
|
|
666
|
+
|
|
667
|
+
The player's own decks, newest first, leaving out private ones.
|
|
668
|
+
|
|
669
|
+
### `Decks.saved(options?): Promise<DecksListResult>`
|
|
670
|
+
|
|
671
|
+
The decks the player bookmarked, then the decks in their playlists, each
|
|
672
|
+
once.
|
|
673
|
+
|
|
674
|
+
Both take `{ limit }`, from 1 through 50 (default 20), and both leave out
|
|
675
|
+
decks whose creator turned portals off. The result is
|
|
676
|
+
`{ status: 'ok', decks }`, where each deck is
|
|
677
|
+
`{ deckId, title, creator: { userId, username } | null, coverUrl }` — the
|
|
678
|
+
same shape as `Portal.info`. `coverUrl` is a `data:` url, since a deck may
|
|
679
|
+
not load remote images; it is null when the deck has no cover. A player
|
|
680
|
+
who is not signed in gets an empty list.
|
|
681
|
+
|
|
682
|
+
```js
|
|
683
|
+
const { decks } = await Decks.mine({ limit: 12 });
|
|
684
|
+
for (const deck of decks) {
|
|
685
|
+
addPickerTile(deck.title, deck.coverUrl, () => placePortal(deck.deckId));
|
|
686
|
+
}
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
## Clipboard
|
|
690
|
+
|
|
691
|
+
The SDK has no clipboard command, and neither Castle host adds one: the
|
|
692
|
+
Castle app has no clipboard bridge, and castle.xyz's player does not grant
|
|
693
|
+
its frame `clipboard-write`. `navigator.clipboard` is best-effort — catch
|
|
694
|
+
its failure and show the text so the player can copy it by hand.
|
|
695
|
+
|
|
619
696
|
## Haptics
|
|
620
697
|
|
|
621
698
|
`Haptics` plays a short device vibration ("buzz") for tactile feedback —
|
|
@@ -651,6 +728,103 @@ Haptics respect the player's settings — if a player has muted haptics,
|
|
|
651
728
|
`play` does nothing and resolves `'unavailable'`. Rapid repeated calls may
|
|
652
729
|
be coalesced, so it's safe to call on frequent events.
|
|
653
730
|
|
|
731
|
+
## Agent
|
|
732
|
+
|
|
733
|
+
`Agent` asks a language model for one chat completion. The host sends it
|
|
734
|
+
through Castle's oracle on the player's own Castle account, so the deck
|
|
735
|
+
holds no key. Agent.chat is limited to Castle admins for now; every other
|
|
736
|
+
player gets `'refused'`.
|
|
737
|
+
|
|
738
|
+
### `Agent.chat(params): Promise<AgentChatResult>`
|
|
739
|
+
|
|
740
|
+
`params`:
|
|
741
|
+
|
|
742
|
+
- `messages` — the conversation, in the OpenAI chat format
|
|
743
|
+
(`{ role, content }`, plus `tool_calls` on an assistant message and
|
|
744
|
+
`tool_call_id` on a `'tool'` message).
|
|
745
|
+
- `tools` — optional function tools, in the OpenAI function-tool schema.
|
|
746
|
+
- `toolChoice` — optional: `'auto'` (the default), `'none'`, `'required'`,
|
|
747
|
+
or `{ type: 'function', function: { name } }` to force one tool.
|
|
748
|
+
- `model` — optional OpenRouter model id. Defaults to
|
|
749
|
+
`'meta/muse-spark-1.3'`.
|
|
750
|
+
- `maxTokens` — optional cap on the reply's length.
|
|
751
|
+
- `reasoning` — optional `{ effort: 'minimal' | 'low' | 'medium' | 'high' }`,
|
|
752
|
+
how hard the model thinks before it answers. Lower effort answers sooner.
|
|
753
|
+
Left out, the model uses its own default. Muse can't turn reasoning off;
|
|
754
|
+
`'minimal'` is its least.
|
|
755
|
+
- `onDelta` — optional callback for the reply as it streams in. It gets
|
|
756
|
+
`{ type: 'text', text }` and `{ type: 'reasoning', text }` pieces, and
|
|
757
|
+
`{ type: 'toolCall', toolCall }` once for each tool call, when its
|
|
758
|
+
arguments are complete. Pieces come in the order the model wrote them.
|
|
759
|
+
- `signal` — optional `AbortSignal`. Aborting it stops the model and
|
|
760
|
+
resolves the call with `'cancelled'` at once.
|
|
761
|
+
|
|
762
|
+
The promise resolves with the whole message when the model finishes. A deck
|
|
763
|
+
runs any tool calls itself, appends the message and one `'tool'` message per
|
|
764
|
+
call, and calls Agent.chat again, until a reply has no tool calls:
|
|
765
|
+
|
|
766
|
+
```js
|
|
767
|
+
import { Agent } from "castle-web-sdk";
|
|
768
|
+
|
|
769
|
+
const tools = [
|
|
770
|
+
{
|
|
771
|
+
type: "function",
|
|
772
|
+
function: {
|
|
773
|
+
name: "roll_dice",
|
|
774
|
+
description: "Roll six-sided dice.",
|
|
775
|
+
parameters: {
|
|
776
|
+
type: "object",
|
|
777
|
+
properties: { count: { type: "integer" } },
|
|
778
|
+
required: ["count"],
|
|
779
|
+
},
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
];
|
|
783
|
+
const messages = [{ role: "user", content: "Roll two dice and add them." }];
|
|
784
|
+
const stop = new AbortController();
|
|
785
|
+
|
|
786
|
+
for (let round = 0; round < 8; round++) {
|
|
787
|
+
const result = await Agent.chat({
|
|
788
|
+
messages,
|
|
789
|
+
tools,
|
|
790
|
+
signal: stop.signal,
|
|
791
|
+
onDelta: (delta) => {
|
|
792
|
+
if (delta.type === "text") {
|
|
793
|
+
showText(delta.text);
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
});
|
|
797
|
+
if (result.status !== "ok") {
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
messages.push(result.message);
|
|
801
|
+
const calls = result.message.tool_calls ?? [];
|
|
802
|
+
if (calls.length === 0) {
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
for (const call of calls) {
|
|
806
|
+
const { count } = JSON.parse(call.function.arguments);
|
|
807
|
+
const faces = Array.from({ length: count }, () => 1 + Math.floor(Math.random() * 6));
|
|
808
|
+
messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(faces) });
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
`AgentChatResult` has a `status`:
|
|
814
|
+
|
|
815
|
+
- `'ok'` — `message` is the assistant's reply: `{ role, content,
|
|
816
|
+
tool_calls? }`, with `content` null when the reply is only tool calls.
|
|
817
|
+
`reasoning` is the model's reasoning summary when it gave one, for
|
|
818
|
+
display; it is not part of `message`. `finishReason` is `'stop'`,
|
|
819
|
+
`'tool_calls'` or `'length'`. `usage` carries `prompt_tokens`,
|
|
820
|
+
`completion_tokens` and `total_tokens`.
|
|
821
|
+
- `'refused'` — the player is not a Castle admin. The model was not called.
|
|
822
|
+
- `'unavailable'` — this host has no route to the oracle.
|
|
823
|
+
- `'cancelled'` — the call's `signal` was aborted.
|
|
824
|
+
- `'error'` — the oracle or the model failed; `reason` says how.
|
|
825
|
+
|
|
826
|
+
Every status but `'ok'` carries a `reason`.
|
|
827
|
+
|
|
654
828
|
## Lifecycle
|
|
655
829
|
|
|
656
830
|
`Lifecycle` tells the host when the deck has painted its first frame, so
|
|
@@ -899,5 +1073,25 @@ Common codes:
|
|
|
899
1073
|
- `CASTLE_HOST_UNAVAILABLE` — the Castle host (the app or website running
|
|
900
1074
|
the deck) didn't handle the request — e.g. it timed out or wasn't
|
|
901
1075
|
reachable. Usually transient; retry or surface a gentle error.
|
|
1076
|
+
- `UNKNOWN_COMMAND` ("Unknown Castle command.") — the Castle app running
|
|
1077
|
+
the deck is a build older than the command. Each app build knows only the
|
|
1078
|
+
commands it shipped with, and only an app update adds one, so keep the
|
|
1079
|
+
deck working without it. The app's last version bump, iOS v1.149, came
|
|
1080
|
+
before every command below, so v1.149 and earlier builds lack all of
|
|
1081
|
+
them. Each date is the commit that added the command to the app's code,
|
|
1082
|
+
not when a build carrying it reached the stores; an app built from code
|
|
1083
|
+
before that commit answers `UNKNOWN_COMMAND`:
|
|
1084
|
+
- `Multiplayer.join` — 2026-08-29
|
|
1085
|
+
- `Store` (every method) — 2026-09-03
|
|
1086
|
+
- `Lifecycle.restoreState` — 2026-09-08 (resolves `null` instead of
|
|
1087
|
+
throwing)
|
|
1088
|
+
- `User.get` — 2026-09-10
|
|
1089
|
+
- `Characters` — 2026-09-20
|
|
1090
|
+
- `Snapshot.offer` — 2026-09-22
|
|
1091
|
+
- `Portal.info` — 2026-09-22 (resolves `unavailable` instead of throwing)
|
|
1092
|
+
- `Agent.chat`, `Decks.mine`, `Decks.saved` — 2026-09-23
|
|
1093
|
+
|
|
1094
|
+
`Leaderboard.show` resolves `unavailable` rather than throwing on an app
|
|
1095
|
+
that lacks it.
|
|
902
1096
|
- `CAULDRON_STORAGE_STALE_GENERATION` — the handle belongs to an older reset
|
|
903
1097
|
generation; reopen explicitly rather than replaying work into the new one.
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AgentChatParams, AgentChatResult } from "./commands";
|
|
2
|
+
export type { AgentChatDelta, AgentChatParams, AgentChatResult, AgentMessage, AgentReasoning, AgentTool, AgentToolCall, AgentToolChoice, AgentUsage, } from "./commands";
|
|
3
|
+
export interface CastleAgentApi {
|
|
4
|
+
chat(params: AgentChatParams): Promise<AgentChatResult>;
|
|
5
|
+
}
|
|
6
|
+
export declare const Agent: CastleAgentApi;
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Agent — one chat completion from a language model, run by the host through
|
|
2
|
+
// Castle's oracle with the player's own Castle account. Messages and tools
|
|
3
|
+
// follow the OpenAI chat completions format. The reply streams: `onDelta` gets
|
|
4
|
+
// text and reasoning as they arrive and each tool call once its arguments are
|
|
5
|
+
// complete, and the promise resolves with the whole message. A deck runs any
|
|
6
|
+
// tool calls itself and calls Agent.chat again with the results.
|
|
7
|
+
//
|
|
8
|
+
// Agent.chat is limited to Castle admins for now: every other player gets
|
|
9
|
+
// `refused`. A host with no route to the oracle answers `unavailable`.
|
|
10
|
+
import { CastleError } from "./errors";
|
|
11
|
+
import { hostRequest } from "./transport";
|
|
12
|
+
async function chat(params) {
|
|
13
|
+
validate(params);
|
|
14
|
+
const onDelta = params.onDelta;
|
|
15
|
+
try {
|
|
16
|
+
return await hostRequest("agent.chat", {
|
|
17
|
+
messages: params.messages,
|
|
18
|
+
tools: params.tools,
|
|
19
|
+
toolChoice: params.toolChoice,
|
|
20
|
+
model: params.model,
|
|
21
|
+
maxTokens: params.maxTokens,
|
|
22
|
+
reasoning: params.reasoning,
|
|
23
|
+
}, {
|
|
24
|
+
onStream: onDelta ? (event) => deliverDelta(onDelta, event) : undefined,
|
|
25
|
+
signal: params.signal,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error instanceof CastleError && error.code === "CANCELLED") {
|
|
30
|
+
return { status: "cancelled", reason: "Agent.chat was cancelled." };
|
|
31
|
+
}
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const REASONING_EFFORTS = ["minimal", "low", "medium", "high"];
|
|
36
|
+
function validate(params) {
|
|
37
|
+
if (!params || typeof params !== "object") {
|
|
38
|
+
invalid("Agent.chat requires { messages }.");
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(params.messages) || params.messages.length === 0) {
|
|
41
|
+
invalid("Agent.chat messages must be a non-empty array.");
|
|
42
|
+
}
|
|
43
|
+
if (params.tools !== undefined && !Array.isArray(params.tools)) {
|
|
44
|
+
invalid("Agent.chat tools must be an array.");
|
|
45
|
+
}
|
|
46
|
+
if (params.model !== undefined && (typeof params.model !== "string" || params.model === "")) {
|
|
47
|
+
invalid("Agent.chat model must be a model id.");
|
|
48
|
+
}
|
|
49
|
+
if (params.maxTokens !== undefined &&
|
|
50
|
+
(!Number.isSafeInteger(params.maxTokens) || params.maxTokens < 1)) {
|
|
51
|
+
invalid("Agent.chat maxTokens must be a positive integer.");
|
|
52
|
+
}
|
|
53
|
+
if (params.reasoning !== undefined && !REASONING_EFFORTS.includes(params.reasoning?.effort)) {
|
|
54
|
+
invalid("Agent.chat reasoning must be { effort: 'minimal' | 'low' | 'medium' | 'high' }.");
|
|
55
|
+
}
|
|
56
|
+
if (params.onDelta !== undefined && typeof params.onDelta !== "function") {
|
|
57
|
+
invalid("Agent.chat onDelta must be a function.");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Keepalives only restart the wait in transport.ts; the deck never sees them.
|
|
61
|
+
function deliverDelta(onDelta, event) {
|
|
62
|
+
const type = event?.type;
|
|
63
|
+
if (type === "text" || type === "reasoning" || type === "toolCall") {
|
|
64
|
+
onDelta(event);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function invalid(message) {
|
|
68
|
+
throw new CastleError({ code: "INVALID_ARGUMENT", message, operation: "Agent.chat" });
|
|
69
|
+
}
|
|
70
|
+
export const Agent = Object.freeze({ chat });
|
package/dist/castle.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export { isEdit } from "./context";
|
|
2
|
+
export { Agent } from "./agent";
|
|
3
|
+
export type { AgentChatDelta, AgentChatParams, AgentChatResult, AgentMessage, AgentReasoning, AgentTool, AgentToolCall, AgentToolChoice, AgentUsage, CastleAgentApi, } from "./agent";
|
|
2
4
|
export { Characters } from "./characters";
|
|
3
5
|
export type { CastleCharactersApi, CharacterHandle, CharacterHistoryOptions, CharacterSendInput, } from "./characters";
|
|
6
|
+
export { Decks } from "./decks";
|
|
7
|
+
export type { CastleDecksApi, DeckSummary, DecksListParams, DecksListResult } from "./decks";
|
|
4
8
|
export { CastleError } from "./errors";
|
|
5
9
|
export { deckPrefixOf, IMPORTS_DIR, IMPORTS_PREFIX, isImportedFile, resolveDeckFile, } from "./files";
|
|
6
10
|
export { Haptics } from "./haptics";
|
|
@@ -11,6 +15,7 @@ export { Lifecycle } from "./lifecycle";
|
|
|
11
15
|
export type { CastleLifecycleApi } from "./lifecycle";
|
|
12
16
|
export { Multiplayer } from "./multiplayer";
|
|
13
17
|
export type { CastleMultiplayerApi, MultiplayerConnection, MultiplayerConnectionState, MultiplayerJoinOptions, MultiplayerRoster, MultiplayerStateMetadata, } from "./multiplayer";
|
|
18
|
+
export type { MultiplayerVoice, VoiceJoinStatus, VoiceMixEntry, VoiceState, } from "./multiplayerVoice";
|
|
14
19
|
export type { MultiplayerSoloReason } from "./commands";
|
|
15
20
|
export { MAX_CLIENT_FRAME_BYTES, MAX_PLAYERS_CAP } from "./multiplayerProtocol";
|
|
16
21
|
export type { ClientByeFrame, ClientHelloFrame, ClientMessageFrame, ClientPingFrame, ClientPongFrame, ClientReplacedFrame, ClientRosterFrame, ClientToPlatformFrame, ClientToPlatformMessageFrame, MultiplayerPlayer, PlayerIdentity, PlatformToClientFrame, SessionPlayer, SessionType, } from "./multiplayerProtocol";
|
package/dist/castle.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Castle Web SDK
|
|
2
2
|
export { isEdit } from "./context";
|
|
3
|
+
export { Agent } from "./agent";
|
|
3
4
|
export { Characters } from "./characters";
|
|
5
|
+
export { Decks } from "./decks";
|
|
4
6
|
export { CastleError } from "./errors";
|
|
5
7
|
export { deckPrefixOf, IMPORTS_DIR, IMPORTS_PREFIX, isImportedFile, resolveDeckFile, } from "./files";
|
|
6
8
|
export { Haptics } from "./haptics";
|
package/dist/commands.d.ts
CHANGED
|
@@ -103,6 +103,14 @@ export interface MultiplayerGetSessionResult {
|
|
|
103
103
|
/** Internal local-serve scope for restoring this Play panel after a reload. */
|
|
104
104
|
devReconnectScope?: string;
|
|
105
105
|
}
|
|
106
|
+
export type VoiceState = "unavailable" | "off" | "joining" | "live";
|
|
107
|
+
export type VoiceJoinStatus = "joined" | "declined" | "unavailable";
|
|
108
|
+
export interface VoiceMixEntry {
|
|
109
|
+
/** 0 (silent) to 1 (full). */
|
|
110
|
+
gain?: number;
|
|
111
|
+
/** -1 (left) to 1 (right). */
|
|
112
|
+
pan?: number;
|
|
113
|
+
}
|
|
106
114
|
export interface CharacterSummary {
|
|
107
115
|
id: string;
|
|
108
116
|
version: string;
|
|
@@ -142,7 +150,97 @@ export interface CharacterHistory {
|
|
|
142
150
|
operations: CharacterOperation[];
|
|
143
151
|
cursor: string | null;
|
|
144
152
|
}
|
|
153
|
+
export interface AgentToolCall {
|
|
154
|
+
id: string;
|
|
155
|
+
type: "function";
|
|
156
|
+
function: {
|
|
157
|
+
name: string;
|
|
158
|
+
arguments: string;
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
export interface AgentMessage {
|
|
162
|
+
role: "system" | "user" | "assistant" | "tool";
|
|
163
|
+
content: string | Json[] | null;
|
|
164
|
+
tool_calls?: AgentToolCall[];
|
|
165
|
+
tool_call_id?: string;
|
|
166
|
+
name?: string;
|
|
167
|
+
}
|
|
168
|
+
export interface AgentTool {
|
|
169
|
+
type: "function";
|
|
170
|
+
function: {
|
|
171
|
+
name: string;
|
|
172
|
+
description?: string;
|
|
173
|
+
parameters?: Json;
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export type AgentToolChoice = "auto" | "none" | "required" | {
|
|
177
|
+
type: "function";
|
|
178
|
+
function: {
|
|
179
|
+
name: string;
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
export type AgentChatDelta = {
|
|
183
|
+
type: "text";
|
|
184
|
+
text: string;
|
|
185
|
+
} | {
|
|
186
|
+
type: "reasoning";
|
|
187
|
+
text: string;
|
|
188
|
+
} | {
|
|
189
|
+
type: "toolCall";
|
|
190
|
+
toolCall: AgentToolCall;
|
|
191
|
+
};
|
|
192
|
+
export type AgentChatStreamEvent = AgentChatDelta | {
|
|
193
|
+
type: "keepalive";
|
|
194
|
+
};
|
|
195
|
+
export interface AgentReasoning {
|
|
196
|
+
effort: "minimal" | "low" | "medium" | "high";
|
|
197
|
+
}
|
|
198
|
+
export interface AgentChatParams {
|
|
199
|
+
messages: AgentMessage[];
|
|
200
|
+
tools?: AgentTool[];
|
|
201
|
+
toolChoice?: AgentToolChoice;
|
|
202
|
+
model?: string;
|
|
203
|
+
maxTokens?: number;
|
|
204
|
+
reasoning?: AgentReasoning;
|
|
205
|
+
onDelta?: (delta: AgentChatDelta) => void;
|
|
206
|
+
signal?: AbortSignal;
|
|
207
|
+
}
|
|
208
|
+
export type AgentChatRequest = Omit<AgentChatParams, "onDelta" | "signal">;
|
|
209
|
+
export interface AgentUsage {
|
|
210
|
+
prompt_tokens: number;
|
|
211
|
+
completion_tokens: number;
|
|
212
|
+
total_tokens: number;
|
|
213
|
+
}
|
|
214
|
+
export type AgentChatResult = {
|
|
215
|
+
status: "ok";
|
|
216
|
+
message: AgentMessage;
|
|
217
|
+
reasoning?: string;
|
|
218
|
+
finishReason: string | null;
|
|
219
|
+
usage?: AgentUsage;
|
|
220
|
+
} | {
|
|
221
|
+
status: "unavailable" | "refused" | "error" | "cancelled";
|
|
222
|
+
reason: string;
|
|
223
|
+
};
|
|
224
|
+
export interface DeckSummary {
|
|
225
|
+
deckId: string;
|
|
226
|
+
title: string;
|
|
227
|
+
creator: {
|
|
228
|
+
userId: string;
|
|
229
|
+
username: string;
|
|
230
|
+
} | null;
|
|
231
|
+
coverUrl: string | null;
|
|
232
|
+
}
|
|
233
|
+
export interface DecksListParams {
|
|
234
|
+
limit?: number | null;
|
|
235
|
+
}
|
|
236
|
+
export interface DecksListResult {
|
|
237
|
+
status: "ok";
|
|
238
|
+
decks: DeckSummary[];
|
|
239
|
+
}
|
|
145
240
|
export interface CommandParams {
|
|
241
|
+
"agent.chat": AgentChatRequest;
|
|
242
|
+
"decks.mine": DecksListParams;
|
|
243
|
+
"decks.saved": DecksListParams;
|
|
146
244
|
"deckStorage.load": Record<string, never>;
|
|
147
245
|
"deckStorage.update": {
|
|
148
246
|
updates: StorageUpdate[];
|
|
@@ -205,6 +303,19 @@ export interface CommandParams {
|
|
|
205
303
|
style: HapticStyle;
|
|
206
304
|
};
|
|
207
305
|
"multiplayer.getSession": MultiplayerGetSessionParams;
|
|
306
|
+
"voice.getState": {
|
|
307
|
+
sessionId: string;
|
|
308
|
+
};
|
|
309
|
+
"voice.requestJoin": {
|
|
310
|
+
sessionId: string;
|
|
311
|
+
};
|
|
312
|
+
"voice.leave": {
|
|
313
|
+
sessionId: string;
|
|
314
|
+
};
|
|
315
|
+
"voice.setMix": {
|
|
316
|
+
sessionId: string;
|
|
317
|
+
mix: Record<string, VoiceMixEntry>;
|
|
318
|
+
};
|
|
208
319
|
"characters.open": {
|
|
209
320
|
characterId: string;
|
|
210
321
|
};
|
|
@@ -279,6 +390,9 @@ export interface CommandParams {
|
|
|
279
390
|
"lifecycle.restoreState": Record<string, never>;
|
|
280
391
|
}
|
|
281
392
|
export interface CommandResult {
|
|
393
|
+
"agent.chat": AgentChatResult;
|
|
394
|
+
"decks.mine": DecksListResult;
|
|
395
|
+
"decks.saved": DecksListResult;
|
|
282
396
|
"deckStorage.load": {
|
|
283
397
|
blob: StorageBlob;
|
|
284
398
|
};
|
|
@@ -324,6 +438,18 @@ export interface CommandResult {
|
|
|
324
438
|
"portal.info": PortalInfoResult;
|
|
325
439
|
"haptics.play": HapticsResult;
|
|
326
440
|
"multiplayer.getSession": MultiplayerGetSessionResult;
|
|
441
|
+
"voice.getState": {
|
|
442
|
+
state: VoiceState;
|
|
443
|
+
};
|
|
444
|
+
"voice.requestJoin": {
|
|
445
|
+
status: VoiceJoinStatus;
|
|
446
|
+
};
|
|
447
|
+
"voice.leave": {
|
|
448
|
+
ok: true;
|
|
449
|
+
};
|
|
450
|
+
"voice.setMix": {
|
|
451
|
+
ok: true;
|
|
452
|
+
};
|
|
327
453
|
"characters.open": CharacterAdmission;
|
|
328
454
|
"characters.send": CharacterOperation;
|
|
329
455
|
"characters.status": CharacterOperation;
|
|
@@ -365,6 +491,8 @@ export interface UserPayload {
|
|
|
365
491
|
color: string | null;
|
|
366
492
|
}
|
|
367
493
|
export type CommandName = keyof CommandParams;
|
|
494
|
+
export declare const COMMAND_TIMEOUTS_MS: Partial<Record<CommandName, number>>;
|
|
495
|
+
export declare const STREAM_IDLE_TIMEOUT_MS = 60000;
|
|
368
496
|
export interface SerializedCommandError {
|
|
369
497
|
code: string;
|
|
370
498
|
message: string;
|
|
@@ -385,6 +513,17 @@ export interface CommandResponseEnvelope {
|
|
|
385
513
|
error?: SerializedCommandError;
|
|
386
514
|
}
|
|
387
515
|
export declare function isResponseEnvelope(value: unknown): value is CommandResponseEnvelope;
|
|
516
|
+
export interface CommandStreamEnvelope {
|
|
517
|
+
castleSdk: typeof CASTLE_SDK_PROTOCOL;
|
|
518
|
+
requestId: string;
|
|
519
|
+
stream: unknown;
|
|
520
|
+
}
|
|
521
|
+
export declare function isStreamEnvelope(value: unknown): value is CommandStreamEnvelope;
|
|
522
|
+
export interface CommandCancelEnvelope {
|
|
523
|
+
castleSdk: typeof CASTLE_SDK_PROTOCOL;
|
|
524
|
+
requestId: string;
|
|
525
|
+
cancel: true;
|
|
526
|
+
}
|
|
388
527
|
export type LifecycleEvent = "ready" | "unloadInterest";
|
|
389
528
|
export interface LifecycleEnvelope {
|
|
390
529
|
castleSdk: typeof CASTLE_SDK_PROTOCOL;
|
|
@@ -404,3 +543,18 @@ export interface UnloadDoneEnvelope {
|
|
|
404
543
|
state?: string;
|
|
405
544
|
};
|
|
406
545
|
}
|
|
546
|
+
export type VoiceEvent = {
|
|
547
|
+
type: "state";
|
|
548
|
+
sessionId: string;
|
|
549
|
+
state: VoiceState;
|
|
550
|
+
} | {
|
|
551
|
+
type: "speaking";
|
|
552
|
+
sessionId: string;
|
|
553
|
+
userIds: string[];
|
|
554
|
+
};
|
|
555
|
+
export interface VoiceEventEnvelope {
|
|
556
|
+
castleSdk: typeof CASTLE_SDK_PROTOCOL;
|
|
557
|
+
voice: VoiceEvent;
|
|
558
|
+
}
|
|
559
|
+
export declare function isVoiceState(value: unknown): value is VoiceState;
|
|
560
|
+
export declare function voiceEventFrom(value: unknown): VoiceEvent | null;
|
package/dist/commands.js
CHANGED
|
@@ -6,6 +6,18 @@
|
|
|
6
6
|
// Marker + protocol version. Doubles as a discriminator so host messages can't
|
|
7
7
|
// be confused with embed.js `castlexyz:` strings or RN console messages.
|
|
8
8
|
export const CASTLE_SDK_PROTOCOL = 1;
|
|
9
|
+
// How long the deck waits for a reply to these commands, in place of the
|
|
10
|
+
// data-command timeout. A deck list fetches every cover on the list. A host
|
|
11
|
+
// from before streaming reads a whole model reply before answering agent.chat.
|
|
12
|
+
export const COMMAND_TIMEOUTS_MS = {
|
|
13
|
+
"agent.chat": 180_000,
|
|
14
|
+
"decks.mine": 30_000,
|
|
15
|
+
"decks.saved": 30_000,
|
|
16
|
+
};
|
|
17
|
+
// Once a host streams a command, each stream envelope restarts the wait at this
|
|
18
|
+
// length instead. The oracle sends a heartbeat every 15 seconds, and the host
|
|
19
|
+
// passes each one on.
|
|
20
|
+
export const STREAM_IDLE_TIMEOUT_MS = 60_000;
|
|
9
21
|
export function isResponseEnvelope(value) {
|
|
10
22
|
if (typeof value !== "object" || value === null)
|
|
11
23
|
return false;
|
|
@@ -14,6 +26,15 @@ export function isResponseEnvelope(value) {
|
|
|
14
26
|
typeof record.requestId === "string" &&
|
|
15
27
|
typeof record.ok === "boolean");
|
|
16
28
|
}
|
|
29
|
+
export function isStreamEnvelope(value) {
|
|
30
|
+
if (typeof value !== "object" || value === null) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const record = value;
|
|
34
|
+
return (record.castleSdk === CASTLE_SDK_PROTOCOL &&
|
|
35
|
+
typeof record.requestId === "string" &&
|
|
36
|
+
"stream" in record);
|
|
37
|
+
}
|
|
17
38
|
export function unloadRequestId(value) {
|
|
18
39
|
if (typeof value !== "object" || value === null)
|
|
19
40
|
return null;
|
|
@@ -23,3 +44,28 @@ export function unloadRequestId(value) {
|
|
|
23
44
|
const unload = record.unload;
|
|
24
45
|
return typeof unload?.unloadId === "string" ? unload.unloadId : null;
|
|
25
46
|
}
|
|
47
|
+
const VOICE_STATES = ["unavailable", "off", "joining", "live"];
|
|
48
|
+
export function isVoiceState(value) {
|
|
49
|
+
return typeof value === "string" && VOICE_STATES.includes(value);
|
|
50
|
+
}
|
|
51
|
+
export function voiceEventFrom(value) {
|
|
52
|
+
if (typeof value !== "object" || value === null)
|
|
53
|
+
return null;
|
|
54
|
+
const record = value;
|
|
55
|
+
if (record.castleSdk !== CASTLE_SDK_PROTOCOL)
|
|
56
|
+
return null;
|
|
57
|
+
const event = record.voice;
|
|
58
|
+
if (typeof event !== "object" || event === null)
|
|
59
|
+
return null;
|
|
60
|
+
if (typeof event.sessionId !== "string")
|
|
61
|
+
return null;
|
|
62
|
+
if (event.type === "state" && isVoiceState(event.state)) {
|
|
63
|
+
return { type: "state", sessionId: event.sessionId, state: event.state };
|
|
64
|
+
}
|
|
65
|
+
if (event.type === "speaking" &&
|
|
66
|
+
Array.isArray(event.userIds) &&
|
|
67
|
+
event.userIds.every((userId) => typeof userId === "string")) {
|
|
68
|
+
return { type: "speaking", sessionId: event.sessionId, userIds: event.userIds.slice() };
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
package/dist/decks.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DecksListParams, DecksListResult } from "./commands";
|
|
2
|
+
export type { DeckSummary, DecksListParams, DecksListResult } from "./commands";
|
|
3
|
+
export interface CastleDecksApi {
|
|
4
|
+
mine(options?: DecksListParams): Promise<DecksListResult>;
|
|
5
|
+
saved(options?: DecksListParams): Promise<DecksListResult>;
|
|
6
|
+
}
|
|
7
|
+
export declare const Decks: CastleDecksApi;
|