@stage5/lumine 0.2.51 → 0.2.52
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/lib/constants.js +4 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +92 -45
package/lib/constants.js
CHANGED
|
@@ -115,6 +115,8 @@ export const BUNDLED_SDK_REFERENCE_URL = new URL(
|
|
|
115
115
|
import.meta.url,
|
|
116
116
|
);
|
|
117
117
|
export const PACKAGE_METADATA_URL = new URL("../package.json", import.meta.url);
|
|
118
|
+
export const LUMINE_WORLD_UPDATE_GUIDANCE = `- For Twinkle.world realtime presence, keep render/input loops local. Queue an update only when relevant state changes, replace any queued snapshot with the newest one, and flush on a fixed 5-15 updates-per-second schedule with at most one updatePresence request in flight. Never call or await updatePresence every animation frame, resend unchanged snapshots, overlap requests, or build a backlog.
|
|
119
|
+
- Send Twinkle.world actions only when the discrete action happens; do not poll or automatically retry them. On WORLD_EVENT_RATE_LIMITED or another recoverable non-session-ended error, drop that attempted presence/action update without an immediate retry and keep the session. Reconnect with backoff only after session.ended or Twinkle.world.isSessionEndedError(error).`;
|
|
118
120
|
export const SDK_REFERENCE_FALLBACK = `${LUMINE_SDK_REFERENCE_MARKER}
|
|
119
121
|
# Twinkle Build SDK Reference
|
|
120
122
|
|
|
@@ -131,6 +133,7 @@ Use these current source-of-truth rules:
|
|
|
131
133
|
- Use Twinkle.aiStories.list/search/get for existing AI Story passage text, story media, and questions.
|
|
132
134
|
- Use Twinkle.ai.chat with history entries shaped as { role, content }, not { text }. Live web search is enabled by default; pass webSearch: false to disable it for the app.
|
|
133
135
|
- Use Twinkle.preview for canvas, WebGL, Three.js, fullscreen, and game layout.
|
|
136
|
+
${LUMINE_WORLD_UPDATE_GUIDANCE}
|
|
134
137
|
- Prefer existing documented Twinkle.* methods over guessing names from old code.
|
|
135
138
|
`;
|
|
136
139
|
export const LUMINE_THREE_VENDOR_GUIDANCE = `- For Three.js, use the first-party core module: import * as THREE from '${BUILD_VENDOR_THREE_MODULE_IMPORT}';
|
|
@@ -285,6 +288,7 @@ lumine save --summary "Describe the change"
|
|
|
285
288
|
${LUMINE_THREE_VENDOR_GUIDANCE}
|
|
286
289
|
- Do not invent or guess Twinkle.* SDK method names. Use ${SDK_REFERENCE_FILE} as the local SDK reference and prefer Twinkle.capabilities checks for gated features.
|
|
287
290
|
- Match storage to update frequency. Twinkle.privateDb and Twinkle.sharedDb are for LOW-frequency durable state only — things that change on a user action (settings, inventory checkpoints, completed quests, saved progress; comments, votes, room settings, submitted records). NEVER write high-frequency or per-frame/per-tick state to them (camera or cursor position, animation state, live movement, presence, autosave every frame/tick). Keep live state in client memory, broadcast realtime/presence via Twinkle.world, and for durable per-user state flush an occasional snapshot on an interval or on exit (never per frame) — e.g. the viewer/user DB or a single latest-snapshot key. The server rate-limits these writes per key and returns 429 on excess; never retry-loop a 429.
|
|
291
|
+
${LUMINE_WORLD_UPDATE_GUIDANCE}
|
|
288
292
|
|
|
289
293
|
## Local Testing (Playwright / browser probes)
|
|
290
294
|
|
package/package.json
CHANGED
package/sdk/BUILD_SDK_INDEX.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Build SDK Index
|
|
2
2
|
|
|
3
|
-
Version: 1.37.
|
|
4
|
-
Updated: 2026-08-
|
|
5
|
-
Generated: 2026-08-
|
|
3
|
+
Version: 1.37.2
|
|
4
|
+
Updated: 2026-08-26
|
|
5
|
+
Generated: 2026-08-26T15:17:11.027Z
|
|
6
6
|
|
|
7
7
|
## Notes
|
|
8
8
|
- This SDK is injected into Build iframes via the Build preview/runtime.
|
|
@@ -341,7 +341,7 @@ renderBattery(policy?.energyPercent, policy?.energySegmentsRemaining);
|
|
|
341
341
|
- Use this for in-app AI replies instead of creating or fetching app-local endpoints such as /api/chat.
|
|
342
342
|
- Example: const chatHistory = conversation.slice(-12).map((entry) => ({ role: entry.role === 'assistant' ? 'assistant' : 'user', content: entry.text }));
|
|
343
343
|
const result = await Twinkle.ai.chat({ message, history: chatHistory, systemPrompt: 'You are a cheerful pirate helper who answers in one sentence.', onText: (text, meta) => renderReply(text), onStatus: (status) => setThinking(status === 'thinking') });
|
|
344
|
-
- async generateObject({ prompt, expectedStructure, thinkingMode, mode, model, instructions, systemPrompt, webSearch, requestId, onText, onStatus } = {}) | scopes: none
|
|
344
|
+
- async generateObject({ prompt, expectedStructure, thinkingMode, mode, model, instructions, systemPrompt, webSearch, requestId, onText, onStatus, onReasoning } = {}) | scopes: none
|
|
345
345
|
- Returns: { object, result, model, provider, thinkingMode, requestedThinkingMode, requestedModel, webSearch, aiUsagePolicy }
|
|
346
346
|
- Generate a validated structured JSON object for app decisions, routing, grading, and game-state logic, with optional live output/status callbacks and web search.
|
|
347
347
|
- Signed-in viewers only.
|
|
@@ -353,13 +353,14 @@ const result = await Twinkle.ai.chat({ message, history: chatHistory, systemProm
|
|
|
353
353
|
- thinkingMode medium uses Grok 4.6 with medium reasoning and consumes normal AI Energy.
|
|
354
354
|
- thinkingMode high without model uses GPT-5.6 Sol with high reasoning and consumes high AI Energy. Explicit model: 'gpt-5.6-sol' selects Sol with xhigh reasoning at the same High AI Energy tier.
|
|
355
355
|
- claude-opus-5 uses Anthropic adaptive High thinking. claude-fable-5 uses Anthropic xhigh thinking and normally consumes more AI Energy for comparable token use. Both debit confirmed provider usage at the High tier.
|
|
356
|
-
- Pass onStatus and/or onText to stream progress from the same structured generation. onStatus receives high-level phases such as thinking, searching_web, responding, validating, and completed.
|
|
356
|
+
- Pass onStatus, onReasoning, and/or onText to stream progress from the same structured generation. onStatus receives high-level phases such as thinking, searching_web, responding, validating, and completed.
|
|
357
|
+
- onReasoning receives accumulated provider-supplied, app-visible reasoning summaries plus { done, delta, requestId, status }. A provider retry may replace the accumulated summary; treat each callback's first argument as the current source of truth. This callback never exposes hidden/private model chain-of-thought.
|
|
357
358
|
- onText receives accumulated structured-output text plus { done, delta, requestId, status }. Partial output is intentionally incomplete and may include provider formatting; parse only when done is true, when the callback receives the canonical object serialized as JSON, and use the resolved object as the source of truth.
|
|
358
|
-
-
|
|
359
|
+
- Put a user-facing field such as producerNotes in expectedStructure when commentary must be part of the validated final object rather than transient reasoning progress.
|
|
359
360
|
- When AI Energy is empty, every automatic or named model choice rejects before new provider work; there is no free fallback mode.
|
|
360
361
|
- Live web search is enabled by default in Medium and High modes. Pass webSearch: false to disable it for the app. Low/Lite Mode remains tool-free; explicitly forcing webSearch: true in Low Mode returns an error.
|
|
361
|
-
- The server validates the final shape; automatic OpenAI/xAI routes can retry malformed output, while explicit Anthropic routes use native JSON Schema output. App code should still validate business-specific enum values.
|
|
362
|
-
- Example: const { object } = await Twinkle.ai.generateObject({ thinkingMode: 'high', model: 'claude-opus-5', prompt: 'Plan the next section from: ' + currentState, expectedStructure: { producerNotes: 'string', action: 'string', confidence: 0 }, onStatus: (phase) => showPhase(phase), onText: (partialJson, meta) => showStructuredProgress(partialJson, meta) });
|
|
362
|
+
- The server validates the final shape; automatic OpenAI/xAI routes can retry malformed output, while explicit Anthropic routes use native JSON Schema output and retry one malformed or shape-invalid result. App code should still validate business-specific enum values.
|
|
363
|
+
- Example: const { object } = await Twinkle.ai.generateObject({ thinkingMode: 'high', model: 'claude-opus-5', prompt: 'Plan the next section from: ' + currentState, expectedStructure: { producerNotes: 'string', action: 'string', confidence: 0 }, onStatus: (phase) => showPhase(phase), onReasoning: (summary, meta) => showReasoningProgress(summary, meta), onText: (partialJson, meta) => showStructuredProgress(partialJson, meta) });
|
|
363
364
|
- onChatStatus(listener) | scopes: none
|
|
364
365
|
- Returns: unsubscribe function
|
|
365
366
|
- Listen to shared runtime AI chat stream events.
|
|
@@ -742,7 +743,8 @@ const result = await Twinkle.characters.chat({ character: 'zero', thinkingMode:
|
|
|
742
743
|
- Signed-in player identity comes from the canonical Twinkle user record; player.profilePicUrl is only used for guests and is returned only when it is a valid absolute HTTPS URL.
|
|
743
744
|
- Subscribe to session.ended and catch updatePresence/send errors. Stop using stale handles and reconnect only when Twinkle.world.isSessionEndedError(error) is true; for other Twinkle.world.isRecoverableSessionError(error) cases, drop the transient presence/action and keep the handle.
|
|
744
745
|
- Use updatePresence for live avatar snapshots and send for lightweight actions such as emotes, interactions, and chat bubbles.
|
|
745
|
-
-
|
|
746
|
+
- Treat the render/input loop as local-only. Queue presence only after relevant fields change, replace any queued snapshot with the newest one, and flush on a fixed 5-15 updates-per-second schedule with at most one updatePresence request in flight. Never call or await updatePresence every animation frame, resend unchanged snapshots, overlap requests, or build a backlog.
|
|
747
|
+
- Send discrete actions only when they happen; do not poll or automatically retry them. The parent limits updatePresence and send together to protect the website connection. WORLD_EVENT_RATE_LIMITED is recoverable: drop that attempted update without an immediate retry and keep the current session.
|
|
746
748
|
- Rooms are addressed by worldKey, roomKey, and instanceId so the contract can later move to sharded or dedicated game backends.
|
|
747
749
|
- Example: const world = await Twinkle.world.join({ roomKey: 'town-square', presence: { x: 0, y: 0, z: 0, facing: 'south' }, player: { name: avatarName } });
|
|
748
750
|
world.subscribe((event) => updateRemotePlayers(event.players));
|
|
@@ -750,8 +752,8 @@ world.updatePresence({ x, y, z, facing });
|
|
|
750
752
|
- isRecoverableSessionError(error) | scopes: none
|
|
751
753
|
- Returns: boolean
|
|
752
754
|
- Return true when a world request error is expected to be handled by app code instead of crashing.
|
|
753
|
-
- Recoverable session errors include ended, missing, socket-disconnected, socket-not-ready, room-missing, preview-updating, and timed-out world session requests.
|
|
754
|
-
- Only session-ended errors prove that the current handle should be discarded.
|
|
755
|
+
- Recoverable session errors include ended, missing, socket-disconnected, socket-not-ready, room-missing, rate-limited, preview-updating, and timed-out world session requests.
|
|
756
|
+
- Only session-ended errors prove that the current handle should be discarded. WORLD_EVENT_RATE_LIMITED, timed-out, or preview-updating presence/action requests must be dropped without reconnecting and without an immediate retry.
|
|
755
757
|
- For durable game state, write through sharedDb/privateDb instead of relying on world presence — but LOW-frequency only (on a user action or an occasional snapshot, never per frame/tick).
|
|
756
758
|
- Example: try {
|
|
757
759
|
await world.updatePresence({ x, y, z, facing });
|
|
@@ -1028,59 +1030,104 @@ Keywords: multiplayer, mmo, town, presence, avatars, movement, three.js, realtim
|
|
|
1028
1030
|
|
|
1029
1031
|
```js
|
|
1030
1032
|
let world = null;
|
|
1033
|
+
let worldConnection = null;
|
|
1031
1034
|
let reconnectTimer = 0;
|
|
1035
|
+
let reconnectDelayMs = 1000;
|
|
1036
|
+
let latestPresence = { x: 0, y: 0, z: 0, facing: 'south', animation: 'idle' };
|
|
1037
|
+
let latestPresenceKey = JSON.stringify(latestPresence);
|
|
1038
|
+
let queuedPresence = null;
|
|
1039
|
+
let presenceInFlight = false;
|
|
1032
1040
|
|
|
1033
1041
|
async function connectWorld() {
|
|
1034
1042
|
if (world) return world;
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1043
|
+
if (worldConnection) return worldConnection;
|
|
1044
|
+
const presenceAtJoin = latestPresence;
|
|
1045
|
+
const presenceKeyAtJoin = latestPresenceKey;
|
|
1046
|
+
worldConnection = Twinkle.world.join({
|
|
1047
|
+
worldKey: 'town', roomKey: 'square', presence: presenceAtJoin,
|
|
1039
1048
|
player: { name: avatarName }
|
|
1040
1049
|
});
|
|
1050
|
+
try {
|
|
1051
|
+
const session = await worldConnection;
|
|
1052
|
+
world = session;
|
|
1053
|
+
reconnectDelayMs = 1000;
|
|
1054
|
+
session.subscribe((event) => {
|
|
1055
|
+
renderPlayers(event.players);
|
|
1056
|
+
if (event.type === 'session.ended') handleWorldDrop(session);
|
|
1057
|
+
if (event.type === 'action.received' && event.action?.type === 'emote') {
|
|
1058
|
+
showEmote(event.sessionId, event.action.data.emote);
|
|
1059
|
+
}
|
|
1060
|
+
});
|
|
1061
|
+
if (latestPresenceKey !== presenceKeyAtJoin) queuedPresence = latestPresence;
|
|
1062
|
+
return session;
|
|
1063
|
+
} finally {
|
|
1064
|
+
worldConnection = null;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1041
1067
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1068
|
+
function handleWorldConnectError(error) {
|
|
1069
|
+
if (Twinkle.world.isRecoverableSessionError(error)) {
|
|
1070
|
+
scheduleReconnect();
|
|
1071
|
+
} else {
|
|
1072
|
+
console.error('World connection failed', error);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function scheduleReconnect() {
|
|
1077
|
+
if (reconnectTimer || world || worldConnection) return;
|
|
1078
|
+
const delay = reconnectDelayMs;
|
|
1079
|
+
reconnectDelayMs = Math.min(30000, reconnectDelayMs * 2);
|
|
1080
|
+
reconnectTimer = setTimeout(() => {
|
|
1081
|
+
reconnectTimer = 0;
|
|
1082
|
+
connectWorld().catch(handleWorldConnectError);
|
|
1083
|
+
}, delay);
|
|
1052
1084
|
}
|
|
1053
1085
|
|
|
1054
|
-
function handleWorldDrop() {
|
|
1086
|
+
function handleWorldDrop(session = world) {
|
|
1087
|
+
if (session && world && world !== session) return;
|
|
1055
1088
|
world = null;
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1089
|
+
queuedPresence = null;
|
|
1090
|
+
scheduleReconnect();
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function queuePresence(next) {
|
|
1094
|
+
const key = JSON.stringify(next);
|
|
1095
|
+
if (key === latestPresenceKey) return;
|
|
1096
|
+
latestPresenceKey = key;
|
|
1097
|
+
latestPresence = next;
|
|
1098
|
+
if (world) queuedPresence = next; // Coalesce to the newest unsent snapshot.
|
|
1062
1099
|
}
|
|
1063
1100
|
|
|
1064
|
-
async function
|
|
1101
|
+
async function flushPresence() {
|
|
1102
|
+
if (presenceInFlight || !queuedPresence || !world) return;
|
|
1103
|
+
const session = world;
|
|
1104
|
+
const next = queuedPresence;
|
|
1105
|
+
queuedPresence = null;
|
|
1106
|
+
presenceInFlight = true;
|
|
1065
1107
|
try {
|
|
1066
|
-
|
|
1067
|
-
// Throttle this in the game loop, for example 5-15 times per second.
|
|
1068
|
-
await session.updatePresence({ x: player.x, y: player.y, z: player.z, facing });
|
|
1108
|
+
await session.updatePresence(next);
|
|
1069
1109
|
} catch (error) {
|
|
1070
1110
|
if (Twinkle.world.isSessionEndedError(error)) {
|
|
1071
|
-
handleWorldDrop();
|
|
1072
|
-
|
|
1111
|
+
handleWorldDrop(session);
|
|
1112
|
+
} else if (!Twinkle.world.isRecoverableSessionError(error)) {
|
|
1113
|
+
console.error('World update failed', error);
|
|
1073
1114
|
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
}
|
|
1078
|
-
throw error;
|
|
1115
|
+
// Recoverable errors drop this transient snapshot without an immediate retry.
|
|
1116
|
+
} finally {
|
|
1117
|
+
presenceInFlight = false;
|
|
1079
1118
|
}
|
|
1080
1119
|
}
|
|
1081
1120
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1121
|
+
// The render/input loop only queues changed local state.
|
|
1122
|
+
function onPlayerStateChanged() {
|
|
1123
|
+
queuePresence({
|
|
1124
|
+
x: player.x, y: player.y, z: player.z,
|
|
1125
|
+
facing, animation: player.animation
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
connectWorld().catch(handleWorldConnectError);
|
|
1130
|
+
setInterval(() => { void flushPresence(); }, 100); // Fixed 10 Hz cap.
|
|
1084
1131
|
```
|
|
1085
1132
|
|
|
1086
1133
|
### Play chess against the computer
|