@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/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';
@@ -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
+ };
@@ -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
+ });