@wibly/sdk 0.1.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 +36 -0
- package/package.json +34 -0
- package/src/client.ts +650 -0
- package/src/consent.ts +78 -0
- package/src/control.ts +44 -0
- package/src/errors.ts +88 -0
- package/src/events.ts +129 -0
- package/src/index.ts +121 -0
- package/src/inference.ts +140 -0
- package/src/lifecycle.ts +72 -0
- package/src/predictive.ts +51 -0
- package/src/react.ts +223 -0
- package/src/store.ts +318 -0
- package/src/submit.ts +92 -0
- package/src/time.ts +58 -0
- package/src/transport.ts +401 -0
- package/src/voice.ts +67 -0
package/src/consent.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consent contract surfaced by the SDK to the Player shell.
|
|
3
|
+
*
|
|
4
|
+
* Per Vibecode Dev Plan §B7 ("`consent.ts` — surface a
|
|
5
|
+
* `consent_required` callback when Persona memory access is rejected
|
|
6
|
+
* for missing consent (B3); the player shell (B6) renders the dialog")
|
|
7
|
+
* and Platform Spec §9.4.
|
|
8
|
+
*
|
|
9
|
+
* **Wire shape.** The Persona Service (chunk B3) returns
|
|
10
|
+
* `{ ok: false, kind: 'consent_required', personaId, playerId }` when
|
|
11
|
+
* a player-scope memory read is rejected for missing consent. The
|
|
12
|
+
* Runtime (chunk B8a) translates that into a server-emitted protocol
|
|
13
|
+
* `event` with `eventType: 'consent_required'` and a payload matching
|
|
14
|
+
* `ConsentRequiredPayload`. The SDK's `useEvent('consent_required',
|
|
15
|
+
* …)` hook (see `events.ts`) surfaces the payload to the Player shell;
|
|
16
|
+
* the shell renders `<ConsentPrompt>` (chunk B6's
|
|
17
|
+
* `apps-shells/player-web/src/components/consent-prompt.tsx`).
|
|
18
|
+
*
|
|
19
|
+
* **Persistence path.** When the player accepts, the shell calls
|
|
20
|
+
* `session.requestConsent(...)` which forwards to the User Portal's
|
|
21
|
+
* consent endpoint (chunk B17a). Until B17a lands the endpoint, the
|
|
22
|
+
* verb returns `Result.err({ kind: 'consent_persistence_not_wired' })`
|
|
23
|
+
* — the prompt callback still fires; only the grant-write is deferred.
|
|
24
|
+
*
|
|
25
|
+
* **Trap discipline** (chunk B7): the consent management surface lives
|
|
26
|
+
* in `apps/portal` (B17a), not here. The SDK exposes the mid-Session
|
|
27
|
+
* prompt verbs only — `requestConsent` (grant a fresh consent),
|
|
28
|
+
* surfaces via `useEvent('consent_required')`. Revoke / list lives on
|
|
29
|
+
* the User Portal Settings page.
|
|
30
|
+
*
|
|
31
|
+
* **Stable contract.** This module is consumed by the Player shell's
|
|
32
|
+
* `consent-flow.ts` reducer (chunk B6) — the type names + payload
|
|
33
|
+
* shape must stay stable across protocol-minor bumps. A breaking
|
|
34
|
+
* change here is a PROTOCOL_VERSION bump.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Payload the SDK surfaces on a `consent_required` callback. Mirrors
|
|
39
|
+
* the shape the Persona Service emits (chunk B3) when `readMemoryEntry`
|
|
40
|
+
* rejects on missing player consent:
|
|
41
|
+
*
|
|
42
|
+
* - `personaId` — the Persona requesting memory access.
|
|
43
|
+
* - `personaDisplayName` — operator-friendly Persona name for the
|
|
44
|
+
* prompt copy.
|
|
45
|
+
* - `scope` — `session | group | player` per the
|
|
46
|
+
* `player_consent_scope` enum.
|
|
47
|
+
*/
|
|
48
|
+
export type ConsentRequiredPayload = {
|
|
49
|
+
readonly personaId: string;
|
|
50
|
+
readonly personaDisplayName: string;
|
|
51
|
+
readonly scope: 'session' | 'group' | 'player';
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The decision the player makes in the prompt. `accepted` triggers the
|
|
56
|
+
* SDK to call `requestConsent` (which forwards to the User Portal
|
|
57
|
+
* endpoint); `declined` records intent locally and emits a lifecycle
|
|
58
|
+
* frame so the Host surface can surface the rejection if it cares.
|
|
59
|
+
*/
|
|
60
|
+
export type ConsentDecision = 'accepted' | 'declined';
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The SDK callback signature the Player shell binds against. The
|
|
64
|
+
* component resolves the returned promise; the SDK forwards the
|
|
65
|
+
* decision to the persistence path (B17a) on accept.
|
|
66
|
+
*/
|
|
67
|
+
export type ConsentRequiredCallback = (
|
|
68
|
+
payload: ConsentRequiredPayload,
|
|
69
|
+
) => Promise<ConsentDecision>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Predicate used by the SDK's event multiplexer to recognise a wire
|
|
73
|
+
* event as a consent prompt. Exported so the player shell's bridge
|
|
74
|
+
* (B6 `session-consent-bridge.tsx`) can reuse the constant when
|
|
75
|
+
* registering its `useEvent` handler.
|
|
76
|
+
*/
|
|
77
|
+
export const CONSENT_REQUIRED_EVENT_TYPE = 'consent_required' as const;
|
|
78
|
+
export type ConsentRequiredEventType = typeof CONSENT_REQUIRED_EVENT_TYPE;
|
package/src/control.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control verbs (per chunk B7 build: "control.ts — host control
|
|
3
|
+
* actions: pause, resume, advance phase. Map to typed emits.").
|
|
4
|
+
*
|
|
5
|
+
* Host-only verbs are surfaced as a `session.host` namespace on the
|
|
6
|
+
* `Session` returned by `createSession`. The verbs translate to
|
|
7
|
+
* `emit` frames with reserved `eventType` strings; the Runtime
|
|
8
|
+
* applies host-gating server-side (chunk B8a wires the auth, the
|
|
9
|
+
* SDK is unprivileged about who calls them).
|
|
10
|
+
*
|
|
11
|
+
* Reserved event types:
|
|
12
|
+
*
|
|
13
|
+
* - `host.pause` — request a pause; the server emits a
|
|
14
|
+
* `lifecycle` `session.paused`.
|
|
15
|
+
* - `host.resume` — resume from pause.
|
|
16
|
+
* - `host.advance_phase` — request the next phase. The Runtime
|
|
17
|
+
* may reject if no transition matches.
|
|
18
|
+
* - `host.reclaim` — reclaim the host slot from a hung
|
|
19
|
+
* host (the player who fires this
|
|
20
|
+
* becomes the new host if allowed).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { EmitPayload } from '@wibly/internal-protocol';
|
|
24
|
+
|
|
25
|
+
export const HOST_EVENT_TYPES = {
|
|
26
|
+
pause: 'host.pause',
|
|
27
|
+
resume: 'host.resume',
|
|
28
|
+
advancePhase: 'host.advance_phase',
|
|
29
|
+
reclaim: 'host.reclaim',
|
|
30
|
+
} as const;
|
|
31
|
+
|
|
32
|
+
export type HostEventType =
|
|
33
|
+
(typeof HOST_EVENT_TYPES)[keyof typeof HOST_EVENT_TYPES];
|
|
34
|
+
|
|
35
|
+
export const buildHostEmitPayload = (
|
|
36
|
+
sessionId: string,
|
|
37
|
+
eventType: HostEventType,
|
|
38
|
+
detail: unknown = {},
|
|
39
|
+
): EmitPayload =>
|
|
40
|
+
({
|
|
41
|
+
sessionId,
|
|
42
|
+
eventType,
|
|
43
|
+
data: detail,
|
|
44
|
+
}) as EmitPayload;
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Categorised SDK error type.
|
|
3
|
+
*
|
|
4
|
+
* The SDK boundary returns `Result<T, SdkError>` rather than throwing
|
|
5
|
+
* (per Vibecode Dev Plan §4.3 and chunk A1). Each kind is a documented
|
|
6
|
+
* recovery shape:
|
|
7
|
+
*
|
|
8
|
+
* - `not_connected` — the caller invoked a method that
|
|
9
|
+
* requires an open WebSocket session
|
|
10
|
+
* before `createSession`'s connection
|
|
11
|
+
* settled (or after `close()`).
|
|
12
|
+
* - `protocol` — the codec rejected an outbound or
|
|
13
|
+
* inbound frame; carries the underlying
|
|
14
|
+
* `@platform/protocol` `ProtocolError`
|
|
15
|
+
* kind for diagnostics.
|
|
16
|
+
* - `submit_rejected` — the Runtime returned a structured
|
|
17
|
+
* `error` frame for a `submit` /
|
|
18
|
+
* `emit`. Carries the server's `code`
|
|
19
|
+
* and `message` verbatim.
|
|
20
|
+
* - `consent_required` — the in-Session prompt fired and the
|
|
21
|
+
* caller is awaiting decision; the
|
|
22
|
+
* player shell renders the dialog.
|
|
23
|
+
* - `consent_persistence_not_wired` — the SDK forwards a granted
|
|
24
|
+
* consent to the User Portal endpoint
|
|
25
|
+
* (chunk B17a); MVP returns this until
|
|
26
|
+
* B17a ships the route. The mid-Session
|
|
27
|
+
* callback still fires so the shell
|
|
28
|
+
* renders the prompt; this error is
|
|
29
|
+
* returned from `session.requestConsent`
|
|
30
|
+
* only.
|
|
31
|
+
* - `runtime_not_wired` — `inference.*` / `voice.*` are routed
|
|
32
|
+
* through the Runtime (chunk B8a). Until
|
|
33
|
+
* B8a binds the inference verbs, the
|
|
34
|
+
* SDK returns this so callers can render
|
|
35
|
+
* the honest-stub state.
|
|
36
|
+
* - `timeout` — an awaited server reply did not land
|
|
37
|
+
* inside the request's deadline.
|
|
38
|
+
*/
|
|
39
|
+
export type SdkErrorKind =
|
|
40
|
+
| 'not_connected'
|
|
41
|
+
| 'protocol'
|
|
42
|
+
| 'submit_rejected'
|
|
43
|
+
| 'consent_required'
|
|
44
|
+
| 'consent_persistence_not_wired'
|
|
45
|
+
| 'runtime_not_wired'
|
|
46
|
+
| 'timeout';
|
|
47
|
+
|
|
48
|
+
export type SdkError =
|
|
49
|
+
| { readonly kind: 'not_connected' }
|
|
50
|
+
| {
|
|
51
|
+
readonly kind: 'protocol';
|
|
52
|
+
readonly cause: string;
|
|
53
|
+
}
|
|
54
|
+
| {
|
|
55
|
+
readonly kind: 'submit_rejected';
|
|
56
|
+
readonly code: string;
|
|
57
|
+
readonly message: string;
|
|
58
|
+
readonly causeMessageId?: string;
|
|
59
|
+
}
|
|
60
|
+
| { readonly kind: 'consent_required' }
|
|
61
|
+
| { readonly kind: 'consent_persistence_not_wired' }
|
|
62
|
+
| { readonly kind: 'runtime_not_wired' }
|
|
63
|
+
| { readonly kind: 'timeout' };
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Render an `SdkError` to a short string. Used by `formatSdkError`
|
|
67
|
+
* call-sites and as the `Result.unwrap` error message — never log the
|
|
68
|
+
* raw error object because the caller may have attached an inner
|
|
69
|
+
* `cause` from `fetch` that contains response bodies or auth headers.
|
|
70
|
+
*/
|
|
71
|
+
export const formatSdkError = (e: SdkError): string => {
|
|
72
|
+
switch (e.kind) {
|
|
73
|
+
case 'not_connected':
|
|
74
|
+
return 'sdk: not connected to a session';
|
|
75
|
+
case 'protocol':
|
|
76
|
+
return `sdk: protocol error: ${e.cause}`;
|
|
77
|
+
case 'submit_rejected':
|
|
78
|
+
return `sdk: server rejected submit (${e.code}: ${e.message})`;
|
|
79
|
+
case 'consent_required':
|
|
80
|
+
return 'sdk: consent required';
|
|
81
|
+
case 'consent_persistence_not_wired':
|
|
82
|
+
return 'sdk: consent persistence endpoint is not yet wired (chunk B17a)';
|
|
83
|
+
case 'runtime_not_wired':
|
|
84
|
+
return 'sdk: runtime endpoint is not yet wired (chunk B8a)';
|
|
85
|
+
case 'timeout':
|
|
86
|
+
return 'sdk: request timed out';
|
|
87
|
+
}
|
|
88
|
+
};
|
package/src/events.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event multiplexer for server-emitted `event` and `lifecycle` frames.
|
|
3
|
+
*
|
|
4
|
+
* The SDK keeps a single subscription table per session (chunk B7
|
|
5
|
+
* trap: "Don't let the AI inline state stores per call site"). The
|
|
6
|
+
* `client.createSession` factory binds the multiplexer to the
|
|
7
|
+
* transport's `onServerMessage` so handlers fire in source-of-truth
|
|
8
|
+
* order.
|
|
9
|
+
*
|
|
10
|
+
* Per chunk B7 build: "events.ts — typed subscriptions for server →
|
|
11
|
+
* client events. Auto-unsubscribe on session end."
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { EventPayload, LifecyclePayload } from '@wibly/internal-protocol';
|
|
15
|
+
|
|
16
|
+
export type EventHandler<P> = (payload: P) => void;
|
|
17
|
+
|
|
18
|
+
export type EventBusObserver = {
|
|
19
|
+
/** Number of currently-registered listeners for the given key. */
|
|
20
|
+
readonly listenerCount: (key: string) => number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type EventBus = {
|
|
24
|
+
readonly onEvent: <P = unknown>(
|
|
25
|
+
eventType: string,
|
|
26
|
+
handler: EventHandler<EventPayload & { data: P }>,
|
|
27
|
+
) => () => void;
|
|
28
|
+
readonly onAnyEvent: (handler: EventHandler<EventPayload>) => () => void;
|
|
29
|
+
readonly onLifecycle: (
|
|
30
|
+
transition: string,
|
|
31
|
+
handler: EventHandler<LifecyclePayload>,
|
|
32
|
+
) => () => void;
|
|
33
|
+
readonly onAnyLifecycle: (
|
|
34
|
+
handler: EventHandler<LifecyclePayload>,
|
|
35
|
+
) => () => void;
|
|
36
|
+
/** Dispatched by the transport when an `event` frame arrives. */
|
|
37
|
+
readonly dispatchEvent: (payload: EventPayload) => void;
|
|
38
|
+
/** Dispatched by the transport when a `lifecycle` frame arrives. */
|
|
39
|
+
readonly dispatchLifecycle: (payload: LifecyclePayload) => void;
|
|
40
|
+
/** Drop all listeners — called on session close. */
|
|
41
|
+
readonly dispose: () => void;
|
|
42
|
+
readonly observer: EventBusObserver;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const EVENT_WILDCARD = '__any__' as const;
|
|
46
|
+
|
|
47
|
+
export const createEventBus = (): EventBus => {
|
|
48
|
+
const eventListeners = new Map<string, Set<EventHandler<EventPayload>>>();
|
|
49
|
+
const lifecycleListeners = new Map<
|
|
50
|
+
string,
|
|
51
|
+
Set<EventHandler<LifecyclePayload>>
|
|
52
|
+
>();
|
|
53
|
+
|
|
54
|
+
const subscribe = <P>(
|
|
55
|
+
table: Map<string, Set<EventHandler<P>>>,
|
|
56
|
+
key: string,
|
|
57
|
+
handler: EventHandler<P>,
|
|
58
|
+
): (() => void) => {
|
|
59
|
+
let bucket = table.get(key);
|
|
60
|
+
if (bucket === undefined) {
|
|
61
|
+
bucket = new Set();
|
|
62
|
+
table.set(key, bucket);
|
|
63
|
+
}
|
|
64
|
+
bucket.add(handler);
|
|
65
|
+
return () => {
|
|
66
|
+
const current = table.get(key);
|
|
67
|
+
if (current === undefined) return;
|
|
68
|
+
current.delete(handler);
|
|
69
|
+
if (current.size === 0) table.delete(key);
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const dispatch = <P>(
|
|
74
|
+
table: Map<string, Set<EventHandler<P>>>,
|
|
75
|
+
key: string,
|
|
76
|
+
payload: P,
|
|
77
|
+
): void => {
|
|
78
|
+
const exact = table.get(key);
|
|
79
|
+
if (exact !== undefined) {
|
|
80
|
+
for (const handler of Array.from(exact)) {
|
|
81
|
+
try {
|
|
82
|
+
handler(payload);
|
|
83
|
+
} catch {
|
|
84
|
+
// listeners must not throw; swallow to avoid cascading.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const wildcard = table.get(EVENT_WILDCARD);
|
|
89
|
+
if (wildcard !== undefined) {
|
|
90
|
+
for (const handler of Array.from(wildcard)) {
|
|
91
|
+
try {
|
|
92
|
+
handler(payload);
|
|
93
|
+
} catch {
|
|
94
|
+
// ignore
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
onEvent: (eventType, handler) =>
|
|
102
|
+
subscribe(
|
|
103
|
+
eventListeners,
|
|
104
|
+
eventType,
|
|
105
|
+
handler as EventHandler<EventPayload>,
|
|
106
|
+
),
|
|
107
|
+
onAnyEvent: (handler) =>
|
|
108
|
+
subscribe(eventListeners, EVENT_WILDCARD, handler),
|
|
109
|
+
onLifecycle: (transition, handler) =>
|
|
110
|
+
subscribe(lifecycleListeners, transition, handler),
|
|
111
|
+
onAnyLifecycle: (handler) =>
|
|
112
|
+
subscribe(lifecycleListeners, EVENT_WILDCARD, handler),
|
|
113
|
+
dispatchEvent: (payload) => {
|
|
114
|
+
dispatch(eventListeners, payload.eventType, payload);
|
|
115
|
+
},
|
|
116
|
+
dispatchLifecycle: (payload) => {
|
|
117
|
+
dispatch(lifecycleListeners, payload.transition, payload);
|
|
118
|
+
},
|
|
119
|
+
dispose: () => {
|
|
120
|
+
eventListeners.clear();
|
|
121
|
+
lifecycleListeners.clear();
|
|
122
|
+
},
|
|
123
|
+
observer: {
|
|
124
|
+
listenerCount: (key) =>
|
|
125
|
+
(eventListeners.get(key)?.size ?? 0) +
|
|
126
|
+
(lifecycleListeners.get(key)?.size ?? 0),
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@platform/sdk` — Studio SDK (client side).
|
|
3
|
+
*
|
|
4
|
+
* Built in chunk B7; see `docs/chunks/B7.md` for the close note.
|
|
5
|
+
*
|
|
6
|
+
* Public entry points:
|
|
7
|
+
*
|
|
8
|
+
* - `createSession(config)` — main factory returning a `Session`.
|
|
9
|
+
* Player shell (chunk B6) + any future Studio SDK consumer wires
|
|
10
|
+
* a single session per surface; the React provider mounts it
|
|
11
|
+
* once.
|
|
12
|
+
*
|
|
13
|
+
* - `@platform/sdk/react` — React provider + hooks
|
|
14
|
+
* (`SessionProvider`, `useSession`, `useSessionState`,
|
|
15
|
+
* `useConnectionState`, `useSessionEvent`, `useSessionLifecycle`,
|
|
16
|
+
* `useConsentPrompt`). The `react` peer dependency is optional;
|
|
17
|
+
* non-React consumers import only from the root entry.
|
|
18
|
+
*
|
|
19
|
+
* The SDK is published to npm as `@wibly/sdk` via
|
|
20
|
+
* `tools/scripts/release-sdk.ts` (chunk B7). The rewrite turns
|
|
21
|
+
* `@platform/*` deps into `@wibly/*` and bumps the SemVer.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export {
|
|
25
|
+
createSession,
|
|
26
|
+
formatSdkError,
|
|
27
|
+
type Session,
|
|
28
|
+
type SessionConfig,
|
|
29
|
+
type SessionState,
|
|
30
|
+
} from './client.js';
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
type SdkError,
|
|
34
|
+
type SdkErrorKind,
|
|
35
|
+
} from './errors.js';
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
CONSENT_REQUIRED_EVENT_TYPE,
|
|
39
|
+
type ConsentDecision,
|
|
40
|
+
type ConsentRequiredCallback,
|
|
41
|
+
type ConsentRequiredEventType,
|
|
42
|
+
type ConsentRequiredPayload,
|
|
43
|
+
} from './consent.js';
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
type ConnectionState,
|
|
47
|
+
type OptimisticProjection,
|
|
48
|
+
type SessionStore,
|
|
49
|
+
type SessionStoreSnapshot,
|
|
50
|
+
applyPatches,
|
|
51
|
+
} from './store.js';
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
type EmitArgs,
|
|
55
|
+
type PendingSubmit,
|
|
56
|
+
type SubmitArgs,
|
|
57
|
+
} from './submit.js';
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
HOST_EVENT_TYPES,
|
|
61
|
+
type HostEventType,
|
|
62
|
+
} from './control.js';
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
INFERENCE_EVENT_PREFIX,
|
|
66
|
+
buildInferenceRequest,
|
|
67
|
+
type InferenceCallInput,
|
|
68
|
+
type InferenceCallSuccess,
|
|
69
|
+
type SdkCallKind,
|
|
70
|
+
type SdkQualityTier,
|
|
71
|
+
type SerialisedInferenceRequest,
|
|
72
|
+
type SessionInference,
|
|
73
|
+
} from './inference.js';
|
|
74
|
+
|
|
75
|
+
export {
|
|
76
|
+
VOICE_EVENT_PREFIX,
|
|
77
|
+
estimateAudioDurationMs,
|
|
78
|
+
type SessionVoice,
|
|
79
|
+
type SpeakInput,
|
|
80
|
+
type SpeakSuccess,
|
|
81
|
+
} from './voice.js';
|
|
82
|
+
|
|
83
|
+
export {
|
|
84
|
+
type EventBus,
|
|
85
|
+
type EventHandler,
|
|
86
|
+
} from './events.js';
|
|
87
|
+
|
|
88
|
+
export {
|
|
89
|
+
type LifecycleBindings,
|
|
90
|
+
} from './lifecycle.js';
|
|
91
|
+
|
|
92
|
+
export {
|
|
93
|
+
createServerTimeSource,
|
|
94
|
+
type RecordedEvent,
|
|
95
|
+
type ServerTimeSource,
|
|
96
|
+
} from './time.js';
|
|
97
|
+
|
|
98
|
+
export {
|
|
99
|
+
computeBackoffMs,
|
|
100
|
+
createTransport,
|
|
101
|
+
decideStateDiffAction,
|
|
102
|
+
type Transport,
|
|
103
|
+
type TransportConfig,
|
|
104
|
+
type TransportObserver,
|
|
105
|
+
type WebSocketFactory,
|
|
106
|
+
type WebSocketLike,
|
|
107
|
+
} from './transport.js';
|
|
108
|
+
|
|
109
|
+
export {
|
|
110
|
+
registerProjection,
|
|
111
|
+
type PendingProjectionHandle,
|
|
112
|
+
} from './predictive.js';
|
|
113
|
+
|
|
114
|
+
// Re-export the JSON-schema utilities from shared so consumers can
|
|
115
|
+
// declare wire-shaped output schemas without pulling shared in directly.
|
|
116
|
+
export {
|
|
117
|
+
jsonSchemaToZod,
|
|
118
|
+
zodToJsonSchema,
|
|
119
|
+
UnsupportedSchemaError,
|
|
120
|
+
type JsonSchema,
|
|
121
|
+
} from '@wibly/internal-shared';
|
package/src/inference.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inference verbs (per chunk B7 build: "inference.ts — typed call
|
|
3
|
+
* helpers: session.inference.host(slots), .judge(slots),
|
|
4
|
+
* .classify(slots), etc. — calls the Gateway through the Runtime
|
|
5
|
+
* (the SDK never holds the gateway-auth key).").
|
|
6
|
+
*
|
|
7
|
+
* The chunk-B7 contract is:
|
|
8
|
+
*
|
|
9
|
+
* 1. The caller invokes `session.inference.<verb>({ slots, output,
|
|
10
|
+
* qualityTier? })`.
|
|
11
|
+
* 2. The SDK serialises any caller-declared Zod output schema to
|
|
12
|
+
* JSON Schema (via `@platform/shared/json-schema.ts`).
|
|
13
|
+
* 3. The SDK sends an `emit` frame with a reserved `inference.*`
|
|
14
|
+
* event type. The Runtime (chunk B8a) receives it, signs the
|
|
15
|
+
* request, forwards to the Gateway, and emits a follow-up
|
|
16
|
+
* `event` frame with the result keyed by the original message
|
|
17
|
+
* id.
|
|
18
|
+
* 4. The SDK matches the inbound event against the pending call's
|
|
19
|
+
* id and resolves the typed result.
|
|
20
|
+
*
|
|
21
|
+
* **Why `emit` and not a new wire kind?** The protocol already
|
|
22
|
+
* carries `emit` for asynchronous client → server work; reserving an
|
|
23
|
+
* `inference.*` namespace on `eventType` keeps the surface compact
|
|
24
|
+
* and avoids a `PROTOCOL_VERSION` bump. The chunk-B8a Runtime is
|
|
25
|
+
* what enforces the gating + signing.
|
|
26
|
+
*
|
|
27
|
+
* Until chunk B8a wires the Runtime-side handler, the SDK's
|
|
28
|
+
* inference verbs return `Err({ kind: 'runtime_not_wired' })`. The
|
|
29
|
+
* SDK still serialises the schema + payload so the chunk-B7 surface
|
|
30
|
+
* is testable (the JSON Schema + outbound `emit` shape are
|
|
31
|
+
* regression-protected); the runtime-side roundtrip lights up with
|
|
32
|
+
* B8a.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { z, ZodTypeAny } from 'zod';
|
|
36
|
+
|
|
37
|
+
import { zodToJsonSchema, type JsonSchema } from '@wibly/internal-shared';
|
|
38
|
+
import type { Result } from '@wibly/internal-shared';
|
|
39
|
+
|
|
40
|
+
import type { SdkError } from './errors.js';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Quality tier surfaced on the SDK boundary. Mirrors
|
|
44
|
+
* `QualityTierSchema` from `@wibly/internal-manifest` but kept as a string
|
|
45
|
+
* literal here so the SDK doesn't pull in the manifest's full Zod
|
|
46
|
+
* runtime (the manifest schemas are heavy; the SDK only needs the
|
|
47
|
+
* literal set).
|
|
48
|
+
*/
|
|
49
|
+
export type SdkQualityTier = 'preview' | 'standard' | 'high';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Recognised call kinds. Matches `CallKindSchema`'s enum; duplicated
|
|
53
|
+
* here to keep the SDK type-only against the manifest package.
|
|
54
|
+
*
|
|
55
|
+
* `host_open_phase` / `narrate_event` / `judge_submissions` /
|
|
56
|
+
* `host_persona_response` / `classify` is the chunk-B2 set; chunk
|
|
57
|
+
* B12 extends it. The SDK accepts any string at runtime — the
|
|
58
|
+
* `CallKind` union exists as an authoring aid in TypeScript.
|
|
59
|
+
*/
|
|
60
|
+
export type SdkCallKind =
|
|
61
|
+
| 'host_open_phase'
|
|
62
|
+
| 'narrate_event'
|
|
63
|
+
| 'judge_submissions'
|
|
64
|
+
| 'host_persona_response'
|
|
65
|
+
| 'classify'
|
|
66
|
+
| (string & {});
|
|
67
|
+
|
|
68
|
+
export type InferenceCallInput<TOutput extends ZodTypeAny> = {
|
|
69
|
+
readonly callKind: SdkCallKind;
|
|
70
|
+
readonly slots: Readonly<Record<string, unknown>>;
|
|
71
|
+
readonly output?: TOutput;
|
|
72
|
+
readonly qualityTier?: SdkQualityTier;
|
|
73
|
+
/**
|
|
74
|
+
* Optional idempotency key. The Runtime forwards it to the
|
|
75
|
+
* Gateway's `metadata.idempotencyKey`. Use case: a host that
|
|
76
|
+
* wants to retry without double-billing.
|
|
77
|
+
*/
|
|
78
|
+
readonly idempotencyKey?: string;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export type InferenceCallSuccess<TOutput extends ZodTypeAny> = {
|
|
82
|
+
/** Raw model output (the Gateway's `output`). */
|
|
83
|
+
readonly output: string;
|
|
84
|
+
/** Parsed structured response. `null` if no schema was provided. */
|
|
85
|
+
readonly structured: TOutput extends ZodTypeAny ? z.infer<TOutput> | null : null;
|
|
86
|
+
/** Gateway usage block. Surface for debug; the operator-side
|
|
87
|
+
* dashboards use the audit ledger instead. */
|
|
88
|
+
readonly usage: {
|
|
89
|
+
readonly model: string;
|
|
90
|
+
readonly tokensIn: number;
|
|
91
|
+
readonly tokensOut: number;
|
|
92
|
+
readonly latencyMs: number;
|
|
93
|
+
readonly costUsd: number;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export type SerialisedInferenceRequest = {
|
|
98
|
+
readonly callKind: SdkCallKind;
|
|
99
|
+
readonly qualityTier: SdkQualityTier;
|
|
100
|
+
readonly slots: Readonly<Record<string, unknown>>;
|
|
101
|
+
readonly outputSchema: JsonSchema | undefined;
|
|
102
|
+
readonly idempotencyKey: string | undefined;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const INFERENCE_EVENT_PREFIX = 'inference.' as const;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Build the wire payload for an inference call. Surfaced as a pure
|
|
109
|
+
* helper so the testkit can assert the serialised shape without
|
|
110
|
+
* spinning a transport.
|
|
111
|
+
*/
|
|
112
|
+
export const buildInferenceRequest = <TOutput extends ZodTypeAny>(
|
|
113
|
+
input: InferenceCallInput<TOutput>,
|
|
114
|
+
): SerialisedInferenceRequest => ({
|
|
115
|
+
callKind: input.callKind,
|
|
116
|
+
qualityTier: input.qualityTier ?? 'standard',
|
|
117
|
+
slots: input.slots,
|
|
118
|
+
outputSchema: input.output ? zodToJsonSchema(input.output) : undefined,
|
|
119
|
+
idempotencyKey: input.idempotencyKey,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The async `Session.inference` namespace. Each verb is bound by
|
|
124
|
+
* `client.ts` to the live transport; the Runtime wires up the
|
|
125
|
+
* server-side response under chunk B8a.
|
|
126
|
+
*/
|
|
127
|
+
export type SessionInference = {
|
|
128
|
+
readonly call: <TOutput extends ZodTypeAny = ZodTypeAny>(
|
|
129
|
+
input: InferenceCallInput<TOutput>,
|
|
130
|
+
) => Promise<Result<InferenceCallSuccess<TOutput>, SdkError>>;
|
|
131
|
+
readonly host: <TOutput extends ZodTypeAny = ZodTypeAny>(
|
|
132
|
+
input: Omit<InferenceCallInput<TOutput>, 'callKind'>,
|
|
133
|
+
) => Promise<Result<InferenceCallSuccess<TOutput>, SdkError>>;
|
|
134
|
+
readonly judge: <TOutput extends ZodTypeAny = ZodTypeAny>(
|
|
135
|
+
input: Omit<InferenceCallInput<TOutput>, 'callKind'>,
|
|
136
|
+
) => Promise<Result<InferenceCallSuccess<TOutput>, SdkError>>;
|
|
137
|
+
readonly classify: <TOutput extends ZodTypeAny = ZodTypeAny>(
|
|
138
|
+
input: Omit<InferenceCallInput<TOutput>, 'callKind'>,
|
|
139
|
+
) => Promise<Result<InferenceCallSuccess<TOutput>, SdkError>>;
|
|
140
|
+
};
|
package/src/lifecycle.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lifecycle hooks for the Studio SDK.
|
|
3
|
+
*
|
|
4
|
+
* Per chunk B7 build: "lifecycle.ts — hooks for session start, phase
|
|
5
|
+
* transitions, session end. Map to lifecycle frames from the
|
|
6
|
+
* server."
|
|
7
|
+
*
|
|
8
|
+
* Lifecycle frames carry a `transition` discriminant (e.g.
|
|
9
|
+
* `session.opened`, `phase.entered:<phaseId>`, `session.closed`) and
|
|
10
|
+
* a `detail` payload whose shape is per-transition. The SDK exposes
|
|
11
|
+
* a typed set of bindings here so a host shell can wire up the
|
|
12
|
+
* common transitions without reaching into the raw event bus.
|
|
13
|
+
*
|
|
14
|
+
* Unknown transitions still surface through `onAnyLifecycle`; the
|
|
15
|
+
* named helpers are convenience for the documented set.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { LifecyclePayload } from '@wibly/internal-protocol';
|
|
19
|
+
|
|
20
|
+
import type { EventBus, EventHandler } from './events.js';
|
|
21
|
+
|
|
22
|
+
export type LifecycleBindings = {
|
|
23
|
+
/** `session.opened` — initial subscribe completed. */
|
|
24
|
+
readonly onSessionOpened: (
|
|
25
|
+
handler: EventHandler<LifecyclePayload>,
|
|
26
|
+
) => () => void;
|
|
27
|
+
/** `session.closed` — the Runtime declared the session terminated. */
|
|
28
|
+
readonly onSessionClosed: (
|
|
29
|
+
handler: EventHandler<LifecyclePayload>,
|
|
30
|
+
) => () => void;
|
|
31
|
+
/** `phase.entered:<id>` — a new phase began. */
|
|
32
|
+
readonly onPhaseEntered: (
|
|
33
|
+
handler: EventHandler<LifecyclePayload>,
|
|
34
|
+
) => () => void;
|
|
35
|
+
/** `phase.exited:<id>` — the active phase ended. */
|
|
36
|
+
readonly onPhaseExited: (
|
|
37
|
+
handler: EventHandler<LifecyclePayload>,
|
|
38
|
+
) => () => void;
|
|
39
|
+
/** `host.reclaimed` — the host slot was assigned to a new player. */
|
|
40
|
+
readonly onHostReclaimed: (
|
|
41
|
+
handler: EventHandler<LifecyclePayload>,
|
|
42
|
+
) => () => void;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const PHASE_ENTERED_PREFIX = 'phase.entered';
|
|
46
|
+
const PHASE_EXITED_PREFIX = 'phase.exited';
|
|
47
|
+
|
|
48
|
+
export const createLifecycleBindings = (bus: EventBus): LifecycleBindings => ({
|
|
49
|
+
onSessionOpened: (handler) => bus.onLifecycle('session.opened', handler),
|
|
50
|
+
onSessionClosed: (handler) => bus.onLifecycle('session.closed', handler),
|
|
51
|
+
onPhaseEntered: (handler) => {
|
|
52
|
+
return bus.onAnyLifecycle((payload) => {
|
|
53
|
+
if (
|
|
54
|
+
payload.transition === PHASE_ENTERED_PREFIX ||
|
|
55
|
+
payload.transition.startsWith(`${PHASE_ENTERED_PREFIX}:`)
|
|
56
|
+
) {
|
|
57
|
+
handler(payload);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
onPhaseExited: (handler) => {
|
|
62
|
+
return bus.onAnyLifecycle((payload) => {
|
|
63
|
+
if (
|
|
64
|
+
payload.transition === PHASE_EXITED_PREFIX ||
|
|
65
|
+
payload.transition.startsWith(`${PHASE_EXITED_PREFIX}:`)
|
|
66
|
+
) {
|
|
67
|
+
handler(payload);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
onHostReclaimed: (handler) => bus.onLifecycle('host.reclaimed', handler),
|
|
72
|
+
});
|