@glassly/cloud-client 0.1.0-dev.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/README.md +35 -0
- package/node/index.ts +54 -0
- package/node/transports.ts +133 -0
- package/package.json +58 -0
- package/react-native/index.ts +38 -0
- package/react-native/transports.ts +231 -0
- package/src/client.ts +265 -0
- package/src/config.ts +92 -0
- package/src/errors.ts +54 -0
- package/src/http.ts +265 -0
- package/src/index.ts +74 -0
- package/src/logger.ts +34 -0
- package/src/modules/auth/auth.ts +553 -0
- package/src/modules/auth/jwt.ts +82 -0
- package/src/modules/auth/token-store.ts +138 -0
- package/src/modules/core/core.ts +219 -0
- package/src/modules/core/reports.ts +153 -0
- package/src/modules/core/support-profile.ts +45 -0
- package/src/modules/runtime/audio-udp.ts +192 -0
- package/src/modules/runtime/camera.ts +196 -0
- package/src/modules/runtime/connection.ts +826 -0
- package/src/modules/runtime/emitter.ts +132 -0
- package/src/modules/runtime/llm.ts +110 -0
- package/src/modules/runtime/maps.ts +105 -0
- package/src/modules/runtime/runtime.ts +585 -0
- package/src/modules/runtime/status.ts +12 -0
- package/src/modules/runtime/subscriptions.ts +140 -0
- package/src/modules/runtime/tts.ts +81 -0
- package/src/timers.ts +23 -0
- package/src/transports.ts +77 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The single typed event emitter behind `cloud.runtime`'s `on*`.
|
|
3
|
+
*
|
|
4
|
+
* There is exactly one emitter so there is one source of truth for runtime
|
|
5
|
+
* events: the friendly per-event methods (`onTranscript`, `onConnected`, ...),
|
|
6
|
+
* the generic `on(event, cb)`, and `onAny(cb)` are all thin wrappers over this.
|
|
7
|
+
* Keying by an event map (`RuntimeEvents`) means event names are checked by the
|
|
8
|
+
* compiler and payloads are typed, so there are no magic strings to mistype.
|
|
9
|
+
*
|
|
10
|
+
* See docs/issues/004-cloud-client/design.md ("src/modules/runtime/emitter.ts").
|
|
11
|
+
*/
|
|
12
|
+
import type {
|
|
13
|
+
QuotaStatusPayload,
|
|
14
|
+
TranscriptionData,
|
|
15
|
+
TranslationData,
|
|
16
|
+
ProtocolError,
|
|
17
|
+
} from "@glassly/cloud-protocol";
|
|
18
|
+
import type { RuntimeSnapshot } from "./status";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The runtime event map: event name to payload type.
|
|
22
|
+
*
|
|
23
|
+
* `transcript` / `translation` carry the canonical wire result types, so a
|
|
24
|
+
* consumer gets exactly what the cloud sent. `connected` has no payload (the
|
|
25
|
+
* fact of connecting is the whole signal), so its type is `void`. `error`
|
|
26
|
+
* carries the wire `ProtocolError` (a non-fatal one; fatal ones close the
|
|
27
|
+
* socket and surface as a reconnect), not a thrown JS error.
|
|
28
|
+
*/
|
|
29
|
+
export interface RuntimeEvents {
|
|
30
|
+
transcript: TranscriptionData;
|
|
31
|
+
translation: TranslationData;
|
|
32
|
+
/** Plan-quota block state changed for a metered stream (stream.quota). */
|
|
33
|
+
quota: QuotaStatusPayload;
|
|
34
|
+
connected: void;
|
|
35
|
+
disconnected: { reason: string };
|
|
36
|
+
status: RuntimeSnapshot;
|
|
37
|
+
error: ProtocolError;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A handler stored for a single event name, payload typed by the map. */
|
|
41
|
+
type Listener<K extends keyof RuntimeEvents> = (data: RuntimeEvents[K]) => void;
|
|
42
|
+
|
|
43
|
+
/** A handler that receives every event, used for forwarding and logging. */
|
|
44
|
+
type AnyListener = (event: keyof RuntimeEvents, data: unknown) => void;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A typed multi-listener emitter for runtime events.
|
|
48
|
+
*
|
|
49
|
+
* Listeners are kept in a `Set` per event so the same handler is not registered
|
|
50
|
+
* twice and `off` is an O(1) removal. `onAny` listeners are kept separately and
|
|
51
|
+
* fired on every `emit`, which is what powers island re-emitting all runtime
|
|
52
|
+
* events without naming each one.
|
|
53
|
+
*/
|
|
54
|
+
export class RuntimeEmitter {
|
|
55
|
+
// One listener set per event name. A `Map` keyed by the event name keeps
|
|
56
|
+
// dispatch touching only the handlers for the event that fired. The stored set
|
|
57
|
+
// is typed loosely (the element type varies per key, which a single field
|
|
58
|
+
// cannot express); the public `on`/`off`/`emit` signatures below are fully
|
|
59
|
+
// typed via the event map, so callers never see the loose inner type.
|
|
60
|
+
private readonly listeners = new Map<
|
|
61
|
+
keyof RuntimeEvents,
|
|
62
|
+
Set<Listener<keyof RuntimeEvents>>
|
|
63
|
+
>();
|
|
64
|
+
|
|
65
|
+
// Listeners that want every event, kept apart so `emit` can fan out to them
|
|
66
|
+
// after the per-event handlers run.
|
|
67
|
+
private readonly anyListeners = new Set<AnyListener>();
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Subscribe to one event. Returns an unsubscribe function, which is the only
|
|
71
|
+
* way callers are expected to detach: holding the returned function means a
|
|
72
|
+
* caller never needs a reference to the original handler to remove it.
|
|
73
|
+
*/
|
|
74
|
+
on<K extends keyof RuntimeEvents>(e: K, cb: Listener<K>): () => void {
|
|
75
|
+
let set = this.listeners.get(e);
|
|
76
|
+
if (!set) {
|
|
77
|
+
set = new Set();
|
|
78
|
+
this.listeners.set(e, set);
|
|
79
|
+
}
|
|
80
|
+
set.add(cb as Listener<keyof RuntimeEvents>);
|
|
81
|
+
return () => this.off(e, cb);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Remove a previously registered handler for one event. A no-op if the handler
|
|
86
|
+
* was never registered, so double-unsubscribe is safe.
|
|
87
|
+
*/
|
|
88
|
+
off<K extends keyof RuntimeEvents>(e: K, cb: Listener<K>): void {
|
|
89
|
+
this.listeners.get(e)?.delete(cb as Listener<keyof RuntimeEvents>);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Subscribe to every event in one handler (for forwarding, iteration, or
|
|
94
|
+
* logging). The payload is `unknown` because it varies per event; a consumer
|
|
95
|
+
* narrows on the event name. Returns an unsubscribe function like `on`.
|
|
96
|
+
*/
|
|
97
|
+
onAny(cb: AnyListener): () => void {
|
|
98
|
+
this.anyListeners.add(cb);
|
|
99
|
+
return () => {
|
|
100
|
+
this.anyListeners.delete(cb);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Deliver an event to its handlers, then to every `onAny` handler.
|
|
106
|
+
*
|
|
107
|
+
* We iterate over a copy of each listener set so a handler that subscribes or
|
|
108
|
+
* unsubscribes during dispatch does not mutate the set we are walking. A throw
|
|
109
|
+
* from one handler must not stop the others or skip the `onAny` fan-out, so
|
|
110
|
+
* each call is isolated and a failure is swallowed (the emitter has no logger;
|
|
111
|
+
* a handler owns its own error handling).
|
|
112
|
+
*/
|
|
113
|
+
emit<K extends keyof RuntimeEvents>(e: K, d: RuntimeEvents[K]): void {
|
|
114
|
+
const set = this.listeners.get(e);
|
|
115
|
+
if (set) {
|
|
116
|
+
for (const cb of [...set]) {
|
|
117
|
+
try {
|
|
118
|
+
(cb as Listener<K>)(d);
|
|
119
|
+
} catch {
|
|
120
|
+
// A misbehaving listener must not break delivery to the others.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
for (const cb of [...this.anyListeners]) {
|
|
125
|
+
try {
|
|
126
|
+
cb(e, d);
|
|
127
|
+
} catch {
|
|
128
|
+
// Same isolation for the catch-all listeners.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Runtime LLM API: provider-neutral completions.
|
|
3
|
+
*
|
|
4
|
+
* A thin client over the runtime's `/api/llm/*` REST endpoints. Plain
|
|
5
|
+
* request/response (no WebSocket push, like maps), so this module only needs
|
|
6
|
+
* the shared HTTP helper. The cloud holds the provider credential; the consumer
|
|
7
|
+
* (a miniapp via `session.ai`) never sees a key or a vendor response shape.
|
|
8
|
+
*
|
|
9
|
+
* The wire types are canonical in the protocol package (the runtime server uses
|
|
10
|
+
* the same ones); re-export them so a host gets them from this module.
|
|
11
|
+
*/
|
|
12
|
+
import type { HttpClient } from "../../http";
|
|
13
|
+
import type {
|
|
14
|
+
LlmCompleteRequest,
|
|
15
|
+
LlmCompleteResult,
|
|
16
|
+
LlmGenerateJobCreated,
|
|
17
|
+
LlmGenerateJobStatus,
|
|
18
|
+
LlmGenerateRequest,
|
|
19
|
+
LlmGenerateResult,
|
|
20
|
+
} from "@glassly/cloud-protocol";
|
|
21
|
+
|
|
22
|
+
export type {
|
|
23
|
+
LlmCompleteRequest,
|
|
24
|
+
LlmCompleteResult,
|
|
25
|
+
LlmGenerateJobCreated,
|
|
26
|
+
LlmGenerateJobStatus,
|
|
27
|
+
LlmGenerateRequest,
|
|
28
|
+
LlmGenerateResult,
|
|
29
|
+
LlmGeneratedImage,
|
|
30
|
+
LlmMessage,
|
|
31
|
+
LlmProvider,
|
|
32
|
+
LlmStopReason,
|
|
33
|
+
LlmTool,
|
|
34
|
+
LlmToolCall,
|
|
35
|
+
LlmToolResult,
|
|
36
|
+
LlmUsage,
|
|
37
|
+
} from "@glassly/cloud-protocol";
|
|
38
|
+
|
|
39
|
+
const COMPLETE_PATH = "/api/llm/complete";
|
|
40
|
+
const GENERATE_PATH = "/api/llm/generate";
|
|
41
|
+
|
|
42
|
+
export interface LlmDeps {
|
|
43
|
+
http: HttpClient;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Extra per-call options that ride as headers rather than in the body. */
|
|
47
|
+
export interface LlmCompleteOptions {
|
|
48
|
+
/**
|
|
49
|
+
* The user's own provider key. Sent as a header so it never lands in a
|
|
50
|
+
* request-body log. When present the platform bills that key instead of its
|
|
51
|
+
* own and the call is not metered.
|
|
52
|
+
*/
|
|
53
|
+
userApiKey?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class Llm {
|
|
57
|
+
private readonly http: HttpClient;
|
|
58
|
+
|
|
59
|
+
constructor(deps: LlmDeps) {
|
|
60
|
+
this.http = deps.http;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Run one completion turn.
|
|
65
|
+
*
|
|
66
|
+
* NOT marked idempotent: a completion is a billed, non-deterministic
|
|
67
|
+
* generation, so a transparent retry after a transient blip could charge
|
|
68
|
+
* twice and return different text. Callers decide whether to retry.
|
|
69
|
+
*/
|
|
70
|
+
complete(req: LlmCompleteRequest, opts: LlmCompleteOptions = {}): Promise<LlmCompleteResult> {
|
|
71
|
+
return this.http.post<LlmCompleteResult>(COMPLETE_PATH, req, {
|
|
72
|
+
headers: opts.userApiKey ? { "x-provider-key": opts.userApiKey } : undefined,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Run one agentic visual-generation call (server-side web search + code
|
|
78
|
+
* execution; returns text plus rendered images). Long-running — the server
|
|
79
|
+
* budgets ~150s of wall clock — and billed like a completion plus a flat
|
|
80
|
+
* surcharge, so like complete() it is NOT idempotent and never auto-retried.
|
|
81
|
+
*/
|
|
82
|
+
generate(req: LlmGenerateRequest, opts: LlmCompleteOptions = {}): Promise<LlmGenerateResult> {
|
|
83
|
+
return this.http.post<LlmGenerateResult>(GENERATE_PATH, req, {
|
|
84
|
+
headers: opts.userApiKey ? { "x-provider-key": opts.userApiKey } : undefined,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Async flavor of generate(): start a pollable job. The job is per runtime
|
|
90
|
+
* instance and short-lived; a host that starts one polls generateJobStatus()
|
|
91
|
+
* every couple of seconds and cancels via cancelGenerateJob() when the
|
|
92
|
+
* consumer goes away. Old clouds without the surface answer 404 — callers
|
|
93
|
+
* fall back to the sync generate().
|
|
94
|
+
*/
|
|
95
|
+
startGenerateJob(req: LlmGenerateRequest, opts: LlmCompleteOptions = {}): Promise<LlmGenerateJobCreated> {
|
|
96
|
+
return this.http.post<LlmGenerateJobCreated>(`${GENERATE_PATH}/jobs`, req, {
|
|
97
|
+
headers: opts.userApiKey ? { "x-provider-key": opts.userApiKey } : undefined,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Poll one job. GET, so the http layer already retries transient blips. */
|
|
102
|
+
generateJobStatus(jobId: string): Promise<LlmGenerateJobStatus> {
|
|
103
|
+
return this.http.get<LlmGenerateJobStatus>(`${GENERATE_PATH}/jobs/${encodeURIComponent(jobId)}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Cancel a running job so an abandoned run stops burning tokens. */
|
|
107
|
+
cancelGenerateJob(jobId: string): Promise<LlmGenerateJobStatus> {
|
|
108
|
+
return this.http.delete<LlmGenerateJobStatus>(`${GENERATE_PATH}/jobs/${encodeURIComponent(jobId)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Runtime maps API: directions + reverse geocoding.
|
|
3
|
+
*
|
|
4
|
+
* A thin client over the runtime's `/api/maps/*` REST endpoints. Both calls are
|
|
5
|
+
* plain request/response (no WebSocket push, unlike camera), so this module only
|
|
6
|
+
* needs the shared HTTP helper. The cloud holds the maps provider token; the
|
|
7
|
+
* consumer (e.g. the navigation miniapp via the SDK) calls these and never sees
|
|
8
|
+
* a provider credential or a vendor-specific response shape.
|
|
9
|
+
*
|
|
10
|
+
* The wire types are canonical in the protocol package (the runtime server uses
|
|
11
|
+
* the same ones); re-export them so a host gets them from this module.
|
|
12
|
+
*/
|
|
13
|
+
import type { HttpClient } from "../../http";
|
|
14
|
+
import type {
|
|
15
|
+
DirectionsRequest,
|
|
16
|
+
DirectionsResult,
|
|
17
|
+
LatLng,
|
|
18
|
+
MapsRenderTokenResult,
|
|
19
|
+
PlaceAutocompleteResult,
|
|
20
|
+
PlaceDetailsResult,
|
|
21
|
+
ReverseGeocodeResult,
|
|
22
|
+
} from "@glassly/cloud-protocol";
|
|
23
|
+
|
|
24
|
+
export type {
|
|
25
|
+
DirectionsRequest,
|
|
26
|
+
DirectionsResult,
|
|
27
|
+
Route,
|
|
28
|
+
RouteStep,
|
|
29
|
+
LatLng,
|
|
30
|
+
TravelMode,
|
|
31
|
+
ManeuverKind,
|
|
32
|
+
RouteAvoidances,
|
|
33
|
+
ReverseGeocodeResult,
|
|
34
|
+
PlaceSuggestion,
|
|
35
|
+
PlaceAutocompleteResult,
|
|
36
|
+
PlaceDetailsResult,
|
|
37
|
+
MapsRenderTokenResult,
|
|
38
|
+
} from "@glassly/cloud-protocol";
|
|
39
|
+
|
|
40
|
+
const DIRECTIONS_PATH = "/api/maps/directions";
|
|
41
|
+
const REVERSE_GEOCODE_PATH = "/api/maps/reverse-geocode";
|
|
42
|
+
const PLACE_AUTOCOMPLETE_PATH = "/api/maps/place-autocomplete";
|
|
43
|
+
const PLACE_DETAILS_PATH = "/api/maps/place-details";
|
|
44
|
+
const RENDER_TOKEN_PATH = "/api/maps/render-token";
|
|
45
|
+
|
|
46
|
+
export interface MapsDeps {
|
|
47
|
+
http: HttpClient;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class Maps {
|
|
51
|
+
private readonly http: HttpClient;
|
|
52
|
+
|
|
53
|
+
constructor(deps: MapsDeps) {
|
|
54
|
+
this.http = deps.http;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Compute routes from origin through stops. Primary route first, alternates after. */
|
|
58
|
+
directions(req: DirectionsRequest): Promise<DirectionsResult> {
|
|
59
|
+
// Idempotent: a directions request is a pure read, safe to retry on a
|
|
60
|
+
// transient network failure.
|
|
61
|
+
return this.http.post<DirectionsResult>(DIRECTIONS_PATH, req, { idempotent: true });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve a coordinate to a short road name (`road`) + full formatted address
|
|
66
|
+
* (`address`). Each is null when none of that kind is found near the coord.
|
|
67
|
+
*/
|
|
68
|
+
reverseGeocode(coord: LatLng): Promise<ReverseGeocodeResult> {
|
|
69
|
+
return this.http.post<ReverseGeocodeResult>(REVERSE_GEOCODE_PATH, coord, {
|
|
70
|
+
idempotent: true,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Type-ahead place search. `sessionToken` groups the keystrokes with the
|
|
76
|
+
* following `placeDetails` pick into one billed search session.
|
|
77
|
+
*/
|
|
78
|
+
placeAutocomplete(req: {
|
|
79
|
+
query: string;
|
|
80
|
+
near?: LatLng;
|
|
81
|
+
sessionToken: string;
|
|
82
|
+
}): Promise<PlaceAutocompleteResult> {
|
|
83
|
+
// Idempotent: autocomplete is a pure read, safe to retry on a transient blip.
|
|
84
|
+
return this.http.post<PlaceAutocompleteResult>(PLACE_AUTOCOMPLETE_PATH, req, {
|
|
85
|
+
idempotent: true,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Resolve an autocomplete suggestion (`placeId` + same `sessionToken`) to coordinates. */
|
|
90
|
+
placeDetails(req: { placeId: string; sessionToken: string }): Promise<PlaceDetailsResult> {
|
|
91
|
+
return this.http.post<PlaceDetailsResult>(PLACE_DETAILS_PATH, req, {
|
|
92
|
+
idempotent: true,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The deployment's public client-side render token, for the map consumers
|
|
98
|
+
* that cannot be proxied (tile rendering, native map SDKs). `token` is null
|
|
99
|
+
* when the deployment serves none — the caller falls back to requiring a
|
|
100
|
+
* user-supplied key.
|
|
101
|
+
*/
|
|
102
|
+
renderToken(): Promise<MapsRenderTokenResult> {
|
|
103
|
+
return this.http.get<MapsRenderTokenResult>(RENDER_TOKEN_PATH);
|
|
104
|
+
}
|
|
105
|
+
}
|