@independo/inderun-web 0.3.0-dev.8 → 0.3.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.
Files changed (47) hide show
  1. package/README.md +12 -10
  2. package/dist/core/engine.d.ts +11 -0
  3. package/dist/core/engine.d.ts.map +1 -1
  4. package/dist/core/engine.js +38 -42
  5. package/dist/core/route-planner.d.ts +16 -13
  6. package/dist/core/route-planner.d.ts.map +1 -1
  7. package/dist/core/route-planner.js +22 -19
  8. package/dist/core/router.d.ts +1 -39
  9. package/dist/core/router.d.ts.map +1 -1
  10. package/dist/core/router.js +12 -206
  11. package/dist/index.d.ts +63 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +47 -2
  14. package/dist/onnx.d.ts +21 -0
  15. package/dist/onnx.d.ts.map +1 -1
  16. package/dist/onnx.js +15 -0
  17. package/dist/openai.d.ts +7 -0
  18. package/dist/openai.d.ts.map +1 -1
  19. package/dist/openai.js +7 -0
  20. package/dist/system-model.d.ts +13 -0
  21. package/dist/system-model.d.ts.map +1 -1
  22. package/dist/system-model.js +13 -0
  23. package/package.json +22 -8
  24. package/src/core/browser-host.ts +339 -0
  25. package/src/core/engine.ts +540 -0
  26. package/src/core/errors.ts +220 -0
  27. package/src/core/event-gate.ts +71 -0
  28. package/src/core/generated/inderun-api.ts +19 -0
  29. package/src/core/host.ts +15 -0
  30. package/src/core/provider.ts +231 -0
  31. package/src/core/registry.ts +48 -0
  32. package/src/core/route-planner.ts +261 -0
  33. package/src/core/router.ts +149 -0
  34. package/src/core/sse.ts +134 -0
  35. package/src/core/telemetry.ts +20 -0
  36. package/src/index.ts +147 -0
  37. package/src/onnx.ts +57 -0
  38. package/src/openai.ts +21 -0
  39. package/src/providers/onnx/provider.ts +300 -0
  40. package/src/providers/onnx/runtime.ts +188 -0
  41. package/src/providers/onnx/transformers-runtime.ts +345 -0
  42. package/src/providers/openai/provider.ts +765 -0
  43. package/src/providers/system-model/chrome-runtime.ts +221 -0
  44. package/src/providers/system-model/provider.ts +260 -0
  45. package/src/providers/system-model/runtime.ts +202 -0
  46. package/src/system-model.ts +49 -0
  47. package/src/web-sdk.ts +114 -0
@@ -0,0 +1,540 @@
1
+ import {
2
+ getTaskRequestValidationIssues,
3
+ type StreamEvent,
4
+ type StreamRunHandle,
5
+ type StreamTerminalOutcome,
6
+ type TaskRequest,
7
+ type TaskResult
8
+ } from "@independo/inderun-contracts";
9
+ import type { HostServices } from "./host.js";
10
+ import type { ProviderAdapter, ProviderCapabilitySnapshot, StreamRun } from "./provider.js";
11
+ import type { IndeRunApi } from "./generated/inderun-api.js";
12
+ import { ProviderRegistry } from "./registry.js";
13
+ import { type RouteSelection, Router } from "./router.js";
14
+ import {
15
+ createCapabilityMismatch,
16
+ createInternal,
17
+ createUnavailable,
18
+ toIndeRunException
19
+ } from "./errors.js";
20
+ import { type TelemetryEvent, type TelemetryService, NoOpTelemetryService } from "./telemetry.js";
21
+ import { EventGate } from "./event-gate.js";
22
+
23
+ /**
24
+ * @deprecated Renamed to `StreamRun`, which is the name the generated
25
+ * `IndeRunApi` surface and the Swift/Kotlin SDKs use. Kept as an alias so the
26
+ * rename is not a breaking change.
27
+ */
28
+ export type StreamHandleResult = StreamRun;
29
+
30
+ /**
31
+ * Main orchestrator SDK entrypoint class.
32
+ * Validates tasks, executes routing rules based on request constraints and connectivity,
33
+ * triggers provider adapters, and logs timing telemetry.
34
+ */
35
+ export class IndeRun implements IndeRunApi {
36
+ private router: Router;
37
+ private telemetryService: TelemetryService;
38
+
39
+ /**
40
+ * Initializes the IndeRun instance.
41
+ * @param registry - Registry filled with active providers.
42
+ * @param hostServices - Services wrapping OS interfaces and indicators.
43
+ * @param telemetryService - Optional custom telemetry service listener.
44
+ */
45
+ constructor(
46
+ private registry: ProviderRegistry,
47
+ private hostServices: HostServices,
48
+ telemetryService?: TelemetryService
49
+ ) {
50
+ this.router = new Router(this.registry);
51
+ this.telemetryService =
52
+ telemetryService || this.hostServices.telemetry || new NoOpTelemetryService();
53
+ }
54
+
55
+ /**
56
+ * Current time from the host clock, falling back to wall-clock time when the
57
+ * host does not supply one.
58
+ */
59
+ private now(): number {
60
+ return this.hostServices.clock ? this.hostServices.clock.now() : Date.now();
61
+ }
62
+
63
+ /**
64
+ * Emits `route_decided` for a completed route selection. Shared by `run()` and
65
+ * `stream()`: the payload is identical for both modes, and the interaction
66
+ * mode is already reflected in the plan the router returned.
67
+ */
68
+ private emitRouteDecided(
69
+ runId: string,
70
+ request: TaskRequest,
71
+ routeSelection: RouteSelection,
72
+ providerCount: number
73
+ ): void {
74
+ this.safeEmit({
75
+ type: "route_decided",
76
+ runId,
77
+ timestamp: this.now(),
78
+ payload: {
79
+ selectedProviderId: routeSelection.routePlan.selectedProviderId,
80
+ fallbackProviderIds: routeSelection.routePlan.fallbackProviderIds,
81
+ rejectedProviders: routeSelection.routePlan.rejectedProviders,
82
+ fallbackAvailable: providerCount > 1,
83
+ taskKind: request.task.kind,
84
+ constraints: request.constraints ?? null,
85
+ preferences: request.preferences ?? null,
86
+ explanation: routeSelection.explanation
87
+ }
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Safely emits a telemetry event by swallowing any internal logging/telemetry system errors.
93
+ * @param event - The telemetry event to emit.
94
+ */
95
+ private safeEmit(event: TelemetryEvent): void {
96
+ try {
97
+ this.telemetryService.emit(event);
98
+ } catch (err) {
99
+ // Telemetry failures must never disrupt primary execution flows.
100
+ console.warn("[IndeRun] Telemetry service emission failed:", err);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Returns a stable, generic description of an error class for privacy-preserving telemetry.
106
+ * @param errorClass - The taxonomy classification of the exception.
107
+ */
108
+ private getStableMessage(errorClass: string): string {
109
+ switch (errorClass) {
110
+ case "CapabilityMismatch":
111
+ return "Provider capability mismatch.";
112
+ case "Offline":
113
+ return "Device is offline.";
114
+ case "AuthError":
115
+ return "Authentication failed.";
116
+ case "RateLimited":
117
+ return "Rate limit exceeded.";
118
+ case "Timeout":
119
+ return "Execution timed out.";
120
+ case "Unavailable":
121
+ return "Provider is unavailable.";
122
+ case "Internal":
123
+ default:
124
+ return "An internal engine error occurred.";
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Orchestrates the execution of a TaskRequest.
130
+ * Performs JSON Schema validation, selects a provider via routing rules,
131
+ * measures total execution time, and attaches telemetry metadata to the outcome.
132
+ * @param request - The canonical task request containing prompts, tasks, and constraints.
133
+ * @returns Canonical task output containing output details and telemetry metadata.
134
+ * @throws {IndeRunException} Standardized error indicating validation, connection, or provider failures.
135
+ */
136
+ async run(request: TaskRequest): Promise<TaskResult> {
137
+ const startTime = this.now();
138
+
139
+ const runId = request.requestId || `run_${Math.random().toString(36).substring(2, 11)}`;
140
+
141
+ try {
142
+ // 1. Validate request payload using schema contracts
143
+ const validationIssues = getTaskRequestValidationIssues(request);
144
+ if (validationIssues.length > 0) {
145
+ const message = `Validation failed for TaskRequest: ${validationIssues
146
+ .map((i) => `${i.path} - ${i.message}`)
147
+ .join("; ")}`;
148
+ throw createInternal(message, {
149
+ runId,
150
+ details: { validationIssues }
151
+ });
152
+ }
153
+
154
+ // 2. Select the route based on constraints and host capabilities
155
+ const routeSelection = await this.router.selectRoute(request, this.hostServices, "run");
156
+ const providers = [routeSelection.provider, ...routeSelection.fallbackProviders];
157
+
158
+ this.emitRouteDecided(runId, request, routeSelection, providers.length);
159
+
160
+ // 3. Execute the run task using the planned provider chain
161
+ const attemptedProviderIds: string[] = [];
162
+ let lastError: unknown;
163
+ for (const [index, provider] of providers.entries()) {
164
+ const providerId = provider.describe().id;
165
+ attemptedProviderIds.push(providerId);
166
+
167
+ try {
168
+ const result = await provider.run(request, {
169
+ runId,
170
+ hostServices: this.hostServices
171
+ });
172
+
173
+ const endTime = this.now();
174
+ const totalMs = endTime - startTime;
175
+
176
+ result.runId = runId;
177
+ result.telemetry = {
178
+ ...result.telemetry,
179
+ providerUsed: providerId,
180
+ totalMs
181
+ };
182
+
183
+ this.safeEmit({
184
+ type: "attempt_succeeded",
185
+ runId,
186
+ timestamp: endTime,
187
+ payload: {
188
+ providerId,
189
+ durationMs: totalMs,
190
+ fallbackOccurred: index > 0,
191
+ attemptedProviderIds
192
+ }
193
+ });
194
+
195
+ return result;
196
+ } catch (err) {
197
+ lastError = toIndeRunException(err, {
198
+ providerId,
199
+ runId,
200
+ details: {
201
+ attemptedProviderIds,
202
+ fallbackOccurred: index > 0,
203
+ routePlan: routeSelection.routePlan
204
+ }
205
+ });
206
+ }
207
+ }
208
+
209
+ const endTime = this.now();
210
+ const totalMs = endTime - startTime;
211
+ const exception = toIndeRunException(lastError ?? new Error("No providers were attempted."), {
212
+ runId,
213
+ details: {
214
+ totalMs,
215
+ attemptedProviderIds,
216
+ fallbackOccurred: providers.length > 1,
217
+ routePlan: routeSelection.routePlan
218
+ }
219
+ });
220
+
221
+ throw exception;
222
+ } catch (error) {
223
+ const exception = toIndeRunException(error, {
224
+ runId,
225
+ details: {
226
+ totalMs:
227
+ typeof (error as { details?: { totalMs?: number } }).details?.totalMs === "number"
228
+ ? (error as { details?: { totalMs?: number } }).details?.totalMs
229
+ : this.now() - startTime
230
+ }
231
+ });
232
+ const endTime = this.now();
233
+ const totalMs =
234
+ typeof (exception as { details?: { totalMs?: number } }).details?.totalMs === "number"
235
+ ? (exception as { details?: { totalMs?: number } }).details?.totalMs
236
+ : endTime - startTime;
237
+
238
+ this.safeEmit({
239
+ type: "attempt_failed",
240
+ runId,
241
+ timestamp: endTime,
242
+ payload: {
243
+ providerId: exception.providerId ?? null,
244
+ durationMs: totalMs,
245
+ errorClass: exception.errorClass,
246
+ message: this.getStableMessage(exception.errorClass)
247
+ }
248
+ });
249
+
250
+ throw exception;
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Orchestrates a Mode 2 streaming execution of a TaskRequest.
256
+ *
257
+ * Routes through the same planner as run(), asking it for a `stream` route: the
258
+ * planner rejects providers that do not declare `supports.streaming` or whose
259
+ * dynamic capability snapshot says they cannot stream here, and reports why. The
260
+ * whole returned chain is therefore mode-compatible and subject to the same
261
+ * privacy, locality, availability, and policy constraints as a Mode 1 route.
262
+ *
263
+ * Fallback to the next provider is allowed only before the first content event
264
+ * has been admitted for the run; once one has been delivered, the run is
265
+ * committed to that provider and any later provider failure becomes a terminal
266
+ * error outcome rather than a silent provider swap — partial output spliced from
267
+ * two providers is undetectable to the caller. Cancellation always forecloses
268
+ * fallback, regardless of commit state, and is idempotent.
269
+ * @param request - The canonical task request containing prompts, tasks, and constraints.
270
+ * @throws {IndeRunException} If validation fails or no eligible streaming provider exists.
271
+ */
272
+ async stream(request: TaskRequest): Promise<StreamRun> {
273
+ const startTime = this.now();
274
+ const now = () => this.now();
275
+ const runId = request.requestId || `run_${Math.random().toString(36).substring(2, 11)}`;
276
+
277
+ const validationIssues = getTaskRequestValidationIssues(request);
278
+ if (validationIssues.length > 0) {
279
+ const message = `Validation failed for TaskRequest: ${validationIssues
280
+ .map((i) => `${i.path} - ${i.message}`)
281
+ .join("; ")}`;
282
+ throw createInternal(message, { runId, details: { validationIssues } });
283
+ }
284
+
285
+ // Route selection failures are the one class of stream error that surfaces as a
286
+ // rejected promise instead of a terminal event, so they must still carry the
287
+ // runId — there is no handle for the caller to correlate them with otherwise.
288
+ let routeSelection: RouteSelection;
289
+ try {
290
+ routeSelection = await this.router.selectRoute(request, this.hostServices, "stream");
291
+ } catch (error) {
292
+ throw toIndeRunException(error, { runId });
293
+ }
294
+ const providers = [routeSelection.provider, ...routeSelection.fallbackProviders];
295
+
296
+ this.emitRouteDecided(runId, request, routeSelection, providers.length);
297
+
298
+ // Defensive only: streaming eligibility is decided by the route planner, which
299
+ // throws with its own rejection reasons when nothing can stream. Reaching here
300
+ // means a planned provider was unregistered between planning and selection.
301
+ if (providers.length === 0) {
302
+ throw createCapabilityMismatch("No eligible provider supports streaming for this request.", {
303
+ runId,
304
+ details: { taskKind: request.task.kind }
305
+ });
306
+ }
307
+
308
+ const controller = new AbortController();
309
+ const gate = new EventGate(runId);
310
+ const hostServices = this.hostServices;
311
+ const safeEmit = this.safeEmit.bind(this);
312
+ const getStableMessage = this.getStableMessage.bind(this);
313
+
314
+ const handle: StreamRunHandle = {
315
+ runId,
316
+ schemaVersion: "1.0",
317
+ startedAt: startTime,
318
+ providerId: providers[0]!.describe().id
319
+ };
320
+
321
+ async function* run(): AsyncGenerator<StreamEvent> {
322
+ const attemptedProviderIds: string[] = [];
323
+ let committed = false;
324
+
325
+ const cancelledOutcome = (partialText: string): StreamTerminalOutcome => {
326
+ const reason =
327
+ typeof controller.signal.reason === "string" ? controller.signal.reason : undefined;
328
+ return {
329
+ outcome: "cancelled",
330
+ runId,
331
+ schemaVersion: "1.0",
332
+ partialText,
333
+ ...(reason !== undefined ? { reason } : {})
334
+ };
335
+ };
336
+
337
+ for (const [index, provider] of providers.entries()) {
338
+ if (controller.signal.aborted) {
339
+ break;
340
+ }
341
+
342
+ const providerId = provider.describe().id;
343
+ attemptedProviderIds.push(providerId);
344
+ safeEmit({
345
+ type: "stream_attempt_started",
346
+ runId,
347
+ timestamp: now(),
348
+ payload: { providerId, fallbackOccurred: index > 0 }
349
+ });
350
+
351
+ let partialText = "";
352
+ try {
353
+ const iterable = (provider as Required<Pick<ProviderAdapter, "stream">>).stream(request, {
354
+ runId,
355
+ hostServices: hostServices,
356
+ signal: controller.signal
357
+ });
358
+
359
+ let terminatedInLoop = false;
360
+ for await (const ev of iterable) {
361
+ if (controller.signal.aborted) {
362
+ break;
363
+ }
364
+
365
+ if (ev.kind === "error") {
366
+ throw ev.error;
367
+ }
368
+
369
+ if (ev.kind === "delta" || ev.kind === "snapshot") {
370
+ partialText = ev.kind === "delta" ? partialText + ev.text : ev.text;
371
+ if (!committed) {
372
+ committed = true;
373
+ safeEmit({
374
+ type: "stream_attempt_succeeded",
375
+ runId,
376
+ timestamp: now(),
377
+ payload: { providerId, attemptedProviderIds }
378
+ });
379
+ }
380
+ const streamEvent = gate.admit({
381
+ timestamp: now(),
382
+ type: ev.kind === "delta" ? "content_delta" : "content_snapshot",
383
+ payload: { text: ev.text }
384
+ });
385
+ if (streamEvent) yield streamEvent;
386
+ continue;
387
+ }
388
+
389
+ // ev.kind === "done"
390
+ const outcome: StreamTerminalOutcome = {
391
+ outcome: "completed",
392
+ runId,
393
+ schemaVersion: "1.0",
394
+ finalText: ev.finalText,
395
+ telemetry: { providerUsed: providerId, totalMs: now() - startTime },
396
+ ...(ev.finishReason !== undefined ? { finishReason: ev.finishReason } : {}),
397
+ ...(ev.usage !== undefined ? { usage: ev.usage } : {})
398
+ };
399
+ const terminal = gate.terminate(outcome, now());
400
+ safeEmit({
401
+ type: "stream_completed",
402
+ runId,
403
+ timestamp: now(),
404
+ payload: { providerId, durationMs: now() - startTime, attemptedProviderIds }
405
+ });
406
+ terminatedInLoop = true;
407
+ if (terminal) yield terminal;
408
+ return;
409
+ }
410
+
411
+ if (terminatedInLoop) {
412
+ return;
413
+ }
414
+
415
+ if (controller.signal.aborted) {
416
+ const terminal = gate.terminate(cancelledOutcome(partialText), now());
417
+ safeEmit({
418
+ type: "stream_cancelled",
419
+ runId,
420
+ timestamp: now(),
421
+ payload: { providerId, attemptedProviderIds }
422
+ });
423
+ if (terminal) yield terminal;
424
+ return;
425
+ }
426
+
427
+ // Provider's iterable ended without a terminal "done"/"error" event.
428
+ throw new Error(`Provider '${providerId}' stream ended without a terminal event.`);
429
+ } catch (err) {
430
+ if (controller.signal.aborted) {
431
+ const terminal = gate.terminate(cancelledOutcome(partialText), now());
432
+ safeEmit({
433
+ type: "stream_cancelled",
434
+ runId,
435
+ timestamp: now(),
436
+ payload: { providerId, attemptedProviderIds }
437
+ });
438
+ if (terminal) yield terminal;
439
+ return;
440
+ }
441
+
442
+ const exception = toIndeRunException(err, {
443
+ providerId,
444
+ runId,
445
+ details: { attemptedProviderIds }
446
+ });
447
+
448
+ if (!committed) {
449
+ safeEmit({
450
+ type: "stream_attempt_failed",
451
+ runId,
452
+ timestamp: now(),
453
+ payload: {
454
+ providerId,
455
+ errorClass: exception.errorClass,
456
+ message: getStableMessage(exception.errorClass)
457
+ }
458
+ });
459
+ continue;
460
+ }
461
+
462
+ const outcome: StreamTerminalOutcome = {
463
+ outcome: "error",
464
+ runId,
465
+ schemaVersion: "1.0",
466
+ error: exception.toContractError(),
467
+ partialText
468
+ };
469
+ const terminal = gate.terminate(outcome, now());
470
+ safeEmit({
471
+ type: "stream_failed",
472
+ runId,
473
+ timestamp: now(),
474
+ payload: {
475
+ providerId,
476
+ errorClass: exception.errorClass,
477
+ message: getStableMessage(exception.errorClass),
478
+ attemptedProviderIds
479
+ }
480
+ });
481
+ if (terminal) yield terminal;
482
+ return;
483
+ }
484
+ }
485
+
486
+ // Every provider failed pre-commit, or cancellation landed before any attempt started.
487
+ if (controller.signal.aborted) {
488
+ const terminal = gate.terminate(cancelledOutcome(""), now());
489
+ safeEmit({
490
+ type: "stream_cancelled",
491
+ runId,
492
+ timestamp: now(),
493
+ payload: { attemptedProviderIds }
494
+ });
495
+ if (terminal) yield terminal;
496
+ return;
497
+ }
498
+
499
+ const exception = createUnavailable("All eligible streaming providers failed.", {
500
+ runId,
501
+ details: { attemptedProviderIds }
502
+ });
503
+ const outcome: StreamTerminalOutcome = {
504
+ outcome: "error",
505
+ runId,
506
+ schemaVersion: "1.0",
507
+ error: exception.toContractError(),
508
+ partialText: ""
509
+ };
510
+ const terminal = gate.terminate(outcome, now());
511
+ safeEmit({
512
+ type: "stream_failed",
513
+ runId,
514
+ timestamp: now(),
515
+ payload: {
516
+ errorClass: exception.errorClass,
517
+ message: getStableMessage(exception.errorClass),
518
+ attemptedProviderIds
519
+ }
520
+ });
521
+ if (terminal) yield terminal;
522
+ }
523
+
524
+ return { handle, events: run(), cancel: (reason?: string) => controller.abort(reason) };
525
+ }
526
+
527
+ /**
528
+ * Reports each registered provider's static descriptor and current dynamic capability check,
529
+ * without executing a task. Useful for UI that shows live provider availability before a run.
530
+ */
531
+ async checkCapabilities(): Promise<ProviderCapabilitySnapshot[]> {
532
+ return Promise.all(
533
+ this.registry.list().map(async (provider) => ({
534
+ providerId: provider.describe().id,
535
+ descriptor: provider.describe(),
536
+ capabilities: await provider.capabilities(this.hostServices)
537
+ }))
538
+ );
539
+ }
540
+ }