@independo/inderun-web 0.3.0-dev.8 → 0.3.0-dev.9

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.
@@ -27,6 +27,17 @@ export declare class IndeRun implements IndeRunApi {
27
27
  * @param telemetryService - Optional custom telemetry service listener.
28
28
  */
29
29
  constructor(registry: ProviderRegistry, hostServices: HostServices, telemetryService?: TelemetryService);
30
+ /**
31
+ * Current time from the host clock, falling back to wall-clock time when the
32
+ * host does not supply one.
33
+ */
34
+ private now;
35
+ /**
36
+ * Emits `route_decided` for a completed route selection. Shared by `run()` and
37
+ * `stream()`: the payload is identical for both modes, and the interaction
38
+ * mode is already reflected in the plan the router returned.
39
+ */
40
+ private emitRouteDecided;
30
41
  /**
31
42
  * Safely emits a telemetry event by swallowing any internal logging/telemetry system errors.
32
43
  * @param event - The telemetry event to emit.
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/core/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,UAAU,EAChB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAmB,0BAA0B,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC5F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAQjD,OAAO,EAAuB,KAAK,gBAAgB,EAAwB,MAAM,gBAAgB,CAAC;AAGlG;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,SAAS,CAAC;AAE3C;;;;GAIG;AACH,qBAAa,OAAQ,YAAW,UAAU;IAWtC,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,YAAY;IAXtB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,gBAAgB,CAAmB;IAE3C;;;;;OAKG;gBAEO,QAAQ,EAAE,gBAAgB,EAC1B,YAAY,EAAE,YAAY,EAClC,gBAAgB,CAAC,EAAE,gBAAgB;IAOrC;;;OAGG;IACH,OAAO,CAAC,QAAQ;IAShB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAoBxB;;;;;;;OAOG;IACG,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAuIpD;;;;;;;;;;;;;;;;;OAiBG;IACG,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC;IA+QtD;;;OAGG;IACG,iBAAiB,IAAI,OAAO,CAAC,0BAA0B,EAAE,CAAC;CASjE"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/core/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,UAAU,EAChB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAmB,0BAA0B,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC5F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAQjD,OAAO,EAAuB,KAAK,gBAAgB,EAAwB,MAAM,gBAAgB,CAAC;AAGlG;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,SAAS,CAAC;AAE3C;;;;GAIG;AACH,qBAAa,OAAQ,YAAW,UAAU;IAWtC,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,YAAY;IAXtB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,gBAAgB,CAAmB;IAE3C;;;;;OAKG;gBAEO,QAAQ,EAAE,gBAAgB,EAC1B,YAAY,EAAE,YAAY,EAClC,gBAAgB,CAAC,EAAE,gBAAgB;IAOrC;;;OAGG;IACH,OAAO,CAAC,GAAG;IAIX;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAuBxB;;;OAGG;IACH,OAAO,CAAC,QAAQ;IAShB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAoBxB;;;;;;;OAOG;IACG,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAsHpD;;;;;;;;;;;;;;;;;OAiBG;IACG,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC;IA+PtD;;;OAGG;IACG,iBAAiB,IAAI,OAAO,CAAC,0BAA0B,EAAE,CAAC;CASjE"}
@@ -27,6 +27,35 @@ export class IndeRun {
27
27
  this.telemetryService =
28
28
  telemetryService || this.hostServices.telemetry || new NoOpTelemetryService();
29
29
  }
30
+ /**
31
+ * Current time from the host clock, falling back to wall-clock time when the
32
+ * host does not supply one.
33
+ */
34
+ now() {
35
+ return this.hostServices.clock ? this.hostServices.clock.now() : Date.now();
36
+ }
37
+ /**
38
+ * Emits `route_decided` for a completed route selection. Shared by `run()` and
39
+ * `stream()`: the payload is identical for both modes, and the interaction
40
+ * mode is already reflected in the plan the router returned.
41
+ */
42
+ emitRouteDecided(runId, request, routeSelection, providerCount) {
43
+ this.safeEmit({
44
+ type: "route_decided",
45
+ runId,
46
+ timestamp: this.now(),
47
+ payload: {
48
+ selectedProviderId: routeSelection.routePlan.selectedProviderId,
49
+ fallbackProviderIds: routeSelection.routePlan.fallbackProviderIds,
50
+ rejectedProviders: routeSelection.routePlan.rejectedProviders,
51
+ fallbackAvailable: providerCount > 1,
52
+ taskKind: request.task.kind,
53
+ constraints: request.constraints ?? null,
54
+ preferences: request.preferences ?? null,
55
+ explanation: routeSelection.explanation
56
+ }
57
+ });
58
+ }
30
59
  /**
31
60
  * Safely emits a telemetry event by swallowing any internal logging/telemetry system errors.
32
61
  * @param event - The telemetry event to emit.
@@ -72,7 +101,7 @@ export class IndeRun {
72
101
  * @throws {IndeRunException} Standardized error indicating validation, connection, or provider failures.
73
102
  */
74
103
  async run(request) {
75
- const startTime = this.hostServices.clock ? this.hostServices.clock.now() : Date.now();
104
+ const startTime = this.now();
76
105
  const runId = request.requestId || `run_${Math.random().toString(36).substring(2, 11)}`;
77
106
  try {
78
107
  // 1. Validate request payload using schema contracts
@@ -89,24 +118,7 @@ export class IndeRun {
89
118
  // 2. Select the route based on constraints and host capabilities
90
119
  const routeSelection = await this.router.selectRoute(request, this.hostServices, "run");
91
120
  const providers = [routeSelection.provider, ...routeSelection.fallbackProviders];
92
- // Emit route_decided event
93
- this.safeEmit({
94
- type: "route_decided",
95
- runId,
96
- timestamp: this.hostServices.clock ? this.hostServices.clock.now() : Date.now(),
97
- payload: {
98
- selectedProviderId: routeSelection.routePlan.selectedProviderId,
99
- fallbackProviderIds: routeSelection.routePlan.fallbackProviderIds,
100
- rejectedProviders: routeSelection.routePlan.rejectedProviders,
101
- fallbackAvailable: providers.length > 1,
102
- taskKind: request.task.kind,
103
- explanation: routeSelection.explanation,
104
- constraints: request.constraints ?? null,
105
- preferences: request.preferences ?? null,
106
- plannerSource: routeSelection.plannerSource,
107
- plannerUnavailableReason: routeSelection.plannerUnavailableReason ?? null
108
- }
109
- });
121
+ this.emitRouteDecided(runId, request, routeSelection, providers.length);
110
122
  // 3. Execute the run task using the planned provider chain
111
123
  const attemptedProviderIds = [];
112
124
  let lastError;
@@ -118,7 +130,7 @@ export class IndeRun {
118
130
  runId,
119
131
  hostServices: this.hostServices
120
132
  });
121
- const endTime = this.hostServices.clock ? this.hostServices.clock.now() : Date.now();
133
+ const endTime = this.now();
122
134
  const totalMs = endTime - startTime;
123
135
  result.runId = runId;
124
136
  result.telemetry = {
@@ -151,7 +163,7 @@ export class IndeRun {
151
163
  });
152
164
  }
153
165
  }
154
- const endTime = this.hostServices.clock ? this.hostServices.clock.now() : Date.now();
166
+ const endTime = this.now();
155
167
  const totalMs = endTime - startTime;
156
168
  const exception = toIndeRunException(lastError ?? new Error("No providers were attempted."), {
157
169
  runId,
@@ -170,10 +182,10 @@ export class IndeRun {
170
182
  details: {
171
183
  totalMs: typeof error.details?.totalMs === "number"
172
184
  ? error.details?.totalMs
173
- : (this.hostServices.clock ? this.hostServices.clock.now() : Date.now()) - startTime
185
+ : this.now() - startTime
174
186
  }
175
187
  });
176
- const endTime = this.hostServices.clock ? this.hostServices.clock.now() : Date.now();
188
+ const endTime = this.now();
177
189
  const totalMs = typeof exception.details?.totalMs === "number"
178
190
  ? exception.details?.totalMs
179
191
  : endTime - startTime;
@@ -210,8 +222,8 @@ export class IndeRun {
210
222
  * @throws {IndeRunException} If validation fails or no eligible streaming provider exists.
211
223
  */
212
224
  async stream(request) {
213
- const startTime = this.hostServices.clock ? this.hostServices.clock.now() : Date.now();
214
- const now = () => (this.hostServices.clock ? this.hostServices.clock.now() : Date.now());
225
+ const startTime = this.now();
226
+ const now = () => this.now();
215
227
  const runId = request.requestId || `run_${Math.random().toString(36).substring(2, 11)}`;
216
228
  const validationIssues = getTaskRequestValidationIssues(request);
217
229
  if (validationIssues.length > 0) {
@@ -231,23 +243,7 @@ export class IndeRun {
231
243
  throw toIndeRunException(error, { runId });
232
244
  }
233
245
  const providers = [routeSelection.provider, ...routeSelection.fallbackProviders];
234
- this.safeEmit({
235
- type: "route_decided",
236
- runId,
237
- timestamp: now(),
238
- payload: {
239
- selectedProviderId: routeSelection.routePlan.selectedProviderId,
240
- fallbackProviderIds: routeSelection.routePlan.fallbackProviderIds,
241
- rejectedProviders: routeSelection.routePlan.rejectedProviders,
242
- fallbackAvailable: providers.length > 1,
243
- taskKind: request.task.kind,
244
- explanation: routeSelection.explanation,
245
- constraints: request.constraints ?? null,
246
- preferences: request.preferences ?? null,
247
- plannerSource: routeSelection.plannerSource,
248
- plannerUnavailableReason: routeSelection.plannerUnavailableReason ?? null
249
- }
250
- });
246
+ this.emitRouteDecided(runId, request, routeSelection, providers.length);
251
247
  // Defensive only: streaming eligibility is decided by the route planner, which
252
248
  // throws with its own rejection reasons when nothing can stream. Reaching here
253
249
  // means a planned provider was unregistered between planning and selection.
@@ -17,25 +17,23 @@ export interface SharedPlannerModule {
17
17
  planRouteJson: (inputJson: string) => string | Promise<string>;
18
18
  }
19
19
  /**
20
- * Coarse, privacy-safe reason the WASM planner produced no route plan. Used
21
- * only for observability (telemetry/logging) — callers still fall back to
22
- * local selection regardless of which reason fired.
20
+ * Coarse, privacy-safe reason the WASM planner produced no route plan. Surfaced
21
+ * on the resulting `Internal` routing error and in the `console.warn`, so an
22
+ * operator can tell a missing bundle chunk from a blocked instantiation.
23
23
  */
24
24
  export type WasmUnavailableReason = "import_failed" | "invalid_module_shape" | "init_failed" | "plan_failed";
25
25
  /**
26
26
  * Result of a {@link RoutePlanner} attempt. `routePlan` is `null` when the
27
- * planner could not produce a plan, in which case `source` is `"unavailable"`
28
- * and `unavailableReason` narrows down why, so callers can log/emit telemetry
29
- * instead of silently swapping planners.
27
+ * planner could not produce a plan, and `unavailableReason` then narrows down
28
+ * why.
30
29
  */
31
30
  export interface PlannerOutcome {
32
31
  routePlan: SharedPlannerRoutePlan | null;
33
- source: "wasm" | "unavailable";
34
32
  unavailableReason?: WasmUnavailableReason;
35
33
  }
36
34
  /**
37
35
  * Strategy that turns planner input into a deterministic route plan, or an
38
- * "unavailable" outcome (callers then fall back to local selection).
36
+ * "unavailable" outcome (which the router reports as a routing failure).
39
37
  */
40
38
  export interface RoutePlanner {
41
39
  planRoute(input: SharedPlannerInput): Promise<PlannerOutcome>;
@@ -55,13 +53,14 @@ export interface ProviderRuntimeSnapshot {
55
53
  * Vite/webpack/Rollup can statically resolve and chunk it — a variable
56
54
  * specifier defeats bundler analysis and silently never loads in the browser
57
55
  * (see issue #109). The module is imported lazily and memoized; if it cannot
58
- * be loaded, `planRoute` resolves an "unavailable" outcome so the engine can
59
- * fall back to local selection, and logs once via `console.warn` so the
60
- * degradation is never fully silent.
56
+ * be loaded, `planRoute` resolves an "unavailable" outcome, which the engine
57
+ * turns into an `Internal` routing failure carrying the reason below. There is
58
+ * no second planner to degrade to — a failure here is a failure to route, not a
59
+ * quieter route — so this also logs once via `console.warn` to name the cause.
61
60
  */
62
61
  export declare class WasmRoutePlanner implements RoutePlanner {
63
62
  private modulePromise?;
64
- private loadFailureReason?;
63
+ private loadFailure?;
65
64
  private hasWarned;
66
65
  planRoute(input: SharedPlannerInput): Promise<PlannerOutcome>;
67
66
  private loadModule;
@@ -70,7 +69,11 @@ export declare class WasmRoutePlanner implements RoutePlanner {
70
69
  }
71
70
  /**
72
71
  * Describes each provider and evaluates its dynamic capabilities against the
73
- * host, returning snapshots sorted by provider id for deterministic planning.
72
+ * host.
73
+ *
74
+ * Registration order is passed through untouched: the shared planner ranks
75
+ * candidates itself and tie-breaks by provider id, so sorting here would only
76
+ * assert an ordering the planner is free to ignore.
74
77
  */
75
78
  export declare function collectProviderRuntimeSnapshots(registryProviders: ProviderAdapter[], hostServices: HostServices): Promise<ProviderRuntimeSnapshot[]>;
76
79
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"route-planner.d.ts","sourceRoot":"","sources":["../../src/core/route-planner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,IAAI,sBAAsB,EACnC,iBAAiB,IAAI,kBAAkB,EACvC,WAAW,EACZ,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EACV,eAAe,EAGhB,MAAM,eAAe,CAAC;AAEvB,YAAY,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,CAAC;AAE3D;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,WAAW,CAAC,kBAAkB,CAAC,iBAAiB,CAAC,CAAC,CAAC;AAEjF;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAChE;AAED;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAC/B,eAAe,GAAG,sBAAsB,GAAG,aAAa,GAAG,aAAa,CAAC;AAE3E;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACzC,MAAM,EAAE,MAAM,GAAG,aAAa,CAAC;IAC/B,iBAAiB,CAAC,EAAE,qBAAqB,CAAC;CAC3C;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC/D;AAED;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,eAAe,CAAC;IAC1B,UAAU,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;IAClE,YAAY,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,CAAC;CACvE;AAED;;;;;;;;;GASG;AACH,qBAAa,gBAAiB,YAAW,YAAY;IACnD,OAAO,CAAC,aAAa,CAAC,CAAsC;IAC5D,OAAO,CAAC,iBAAiB,CAAC,CAAwB;IAClD,OAAO,CAAC,SAAS,CAAS;IAEpB,SAAS,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC;YAkBrD,UAAU;YAOV,YAAY;IA6B1B,OAAO,CAAC,QAAQ;CAUjB;AAED;;;GAGG;AACH,wBAAsB,+BAA+B,CACnD,iBAAiB,EAAE,eAAe,EAAE,EACpC,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAepC;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,uBAAuB,EAAE,EACpC,aAAa,EAAE,OAAO,EACtB,eAAe,GAAE,eAAuB,GACvC,kBAAkB,CAuBpB"}
1
+ {"version":3,"file":"route-planner.d.ts","sourceRoot":"","sources":["../../src/core/route-planner.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,IAAI,sBAAsB,EACnC,iBAAiB,IAAI,kBAAkB,EACvC,WAAW,EACZ,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EACV,eAAe,EAGhB,MAAM,eAAe,CAAC;AAEvB,YAAY,EAAE,kBAAkB,EAAE,sBAAsB,EAAE,CAAC;AAE3D;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,WAAW,CAAC,kBAAkB,CAAC,iBAAiB,CAAC,CAAC,CAAC;AAEjF;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACrC,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAChE;AAED;;;;GAIG;AACH,MAAM,MAAM,qBAAqB,GAC/B,eAAe,GAAG,sBAAsB,GAAG,aAAa,GAAG,aAAa,CAAC;AAE3E;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,sBAAsB,GAAG,IAAI,CAAC;IACzC,iBAAiB,CAAC,EAAE,qBAAqB,CAAC;CAC3C;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC/D;AAED;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,eAAe,CAAC;IAC1B,UAAU,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;IAClE,YAAY,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,CAAC;CACvE;AAED;;;;;;;;;;GAUG;AACH,qBAAa,gBAAiB,YAAW,YAAY;IACnD,OAAO,CAAC,aAAa,CAAC,CAAsC;IAC5D,OAAO,CAAC,WAAW,CAAC,CAAqD;IACzE,OAAO,CAAC,SAAS,CAAS;IAEpB,SAAS,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC;YAoBrD,UAAU;YAOV,YAAY;IA0B1B,OAAO,CAAC,QAAQ;CAOjB;AAED;;;;;;;GAOG;AACH,wBAAsB,+BAA+B,CACnD,iBAAiB,EAAE,eAAe,EAAE,EACpC,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,uBAAuB,EAAE,CAAC,CAapC;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,WAAW,EACpB,SAAS,EAAE,uBAAuB,EAAE,EACpC,aAAa,EAAE,OAAO,EACtB,eAAe,GAAE,eAAuB,GACvC,kBAAkB,CAuBpB"}
@@ -4,29 +4,32 @@
4
4
  * Vite/webpack/Rollup can statically resolve and chunk it — a variable
5
5
  * specifier defeats bundler analysis and silently never loads in the browser
6
6
  * (see issue #109). The module is imported lazily and memoized; if it cannot
7
- * be loaded, `planRoute` resolves an "unavailable" outcome so the engine can
8
- * fall back to local selection, and logs once via `console.warn` so the
9
- * degradation is never fully silent.
7
+ * be loaded, `planRoute` resolves an "unavailable" outcome, which the engine
8
+ * turns into an `Internal` routing failure carrying the reason below. There is
9
+ * no second planner to degrade to — a failure here is a failure to route, not a
10
+ * quieter route — so this also logs once via `console.warn` to name the cause.
10
11
  */
11
12
  export class WasmRoutePlanner {
12
13
  modulePromise;
13
- loadFailureReason;
14
+ loadFailure;
14
15
  hasWarned = false;
15
16
  async planRoute(input) {
16
17
  const module = await this.loadModule();
17
18
  if (!module) {
18
- const unavailableReason = this.loadFailureReason ?? "import_failed";
19
- this.warnOnce(unavailableReason);
20
- return { routePlan: null, source: "unavailable", unavailableReason };
19
+ // The load path only records why it failed; warning here covers both the
20
+ // first attempt and every later call served from the memoized promise.
21
+ const unavailableReason = this.loadFailure?.reason ?? "import_failed";
22
+ this.warnOnce(unavailableReason, this.loadFailure?.error);
23
+ return { routePlan: null, unavailableReason };
21
24
  }
22
25
  try {
23
26
  const json = JSON.stringify(input);
24
27
  const result = await module.planRouteJson(json);
25
- return { routePlan: JSON.parse(result), source: "wasm" };
28
+ return { routePlan: JSON.parse(result) };
26
29
  }
27
30
  catch (error) {
28
31
  this.warnOnce("plan_failed", error);
29
- return { routePlan: null, source: "unavailable", unavailableReason: "plan_failed" };
32
+ return { routePlan: null, unavailableReason: "plan_failed" };
30
33
  }
31
34
  }
32
35
  async loadModule() {
@@ -41,13 +44,11 @@ export class WasmRoutePlanner {
41
44
  mod = (await import("@independo/inderun-route-core-wasm"));
42
45
  }
43
46
  catch (error) {
44
- this.loadFailureReason = "import_failed";
45
- this.warnOnce("import_failed", error);
47
+ this.loadFailure = { reason: "import_failed", error };
46
48
  return null;
47
49
  }
48
50
  if (typeof mod.planRouteJson !== "function") {
49
- this.loadFailureReason = "invalid_module_shape";
50
- this.warnOnce("invalid_module_shape");
51
+ this.loadFailure = { reason: "invalid_module_shape" };
51
52
  return null;
52
53
  }
53
54
  if (mod.initSharedCore) {
@@ -55,8 +56,7 @@ export class WasmRoutePlanner {
55
56
  await mod.initSharedCore();
56
57
  }
57
58
  catch (error) {
58
- this.loadFailureReason = "init_failed";
59
- this.warnOnce("init_failed", error);
59
+ this.loadFailure = { reason: "init_failed", error };
60
60
  return null;
61
61
  }
62
62
  }
@@ -67,15 +67,19 @@ export class WasmRoutePlanner {
67
67
  return;
68
68
  }
69
69
  this.hasWarned = true;
70
- console.warn(`[IndeRun] WASM route planner unavailable (${reason}); falling back to local route selection.`, error);
70
+ console.warn(`[IndeRun] WASM route planner unavailable (${reason}); routing will fail.`, error);
71
71
  }
72
72
  }
73
73
  /**
74
74
  * Describes each provider and evaluates its dynamic capabilities against the
75
- * host, returning snapshots sorted by provider id for deterministic planning.
75
+ * host.
76
+ *
77
+ * Registration order is passed through untouched: the shared planner ranks
78
+ * candidates itself and tie-breaks by provider id, so sorting here would only
79
+ * assert an ordering the planner is free to ignore.
76
80
  */
77
81
  export async function collectProviderRuntimeSnapshots(registryProviders, hostServices) {
78
- const snapshots = await Promise.all(registryProviders.map(async (provider) => {
82
+ return Promise.all(registryProviders.map(async (provider) => {
79
83
  const descriptor = provider.describe();
80
84
  const capabilities = await provider.capabilities(hostServices);
81
85
  return {
@@ -84,7 +88,6 @@ export async function collectProviderRuntimeSnapshots(registryProviders, hostSer
84
88
  capabilities: toSharedPlannerCapabilities(capabilities, descriptor, provider)
85
89
  };
86
90
  }));
87
- return snapshots.sort((left, right) => left.descriptor.id.localeCompare(right.descriptor.id));
88
91
  }
89
92
  /**
90
93
  * Assembles the normalized {@link SharedPlannerInput} the shared route core
@@ -2,7 +2,7 @@ import type { TaskRequest } from "@independo/inderun-contracts";
2
2
  import type { HostServices } from "./host.js";
3
3
  import type { ProviderAdapter } from "./provider.js";
4
4
  import type { ProviderRegistry } from "./registry.js";
5
- import { type InteractionMode, type RoutePlanner, type SharedPlannerRoutePlan, type WasmUnavailableReason } from "./route-planner.js";
5
+ import { type InteractionMode, type RoutePlanner, type SharedPlannerRoutePlan } from "./route-planner.js";
6
6
  /**
7
7
  * Output structure from the Router containing the chosen provider and a route selection description.
8
8
  */
@@ -23,16 +23,6 @@ export interface RouteSelection {
23
23
  * Explanation detailing the selection decision. Useful for debugging and telemetry.
24
24
  */
25
25
  explanation: string;
26
- /**
27
- * Which planner produced `routePlan`: the shared Rust/WASM core, or the
28
- * local TypeScript fallback used when the WASM planner is unavailable.
29
- */
30
- plannerSource: "wasm" | "fallback";
31
- /**
32
- * Set when `plannerSource` is `"fallback"` because the WASM planner failed;
33
- * `undefined` when the fallback was not caused by a planner failure.
34
- */
35
- plannerUnavailableReason?: WasmUnavailableReason;
36
26
  }
37
27
  /**
38
28
  * Routing engine module. Filters registered providers against task types, request constraints,
@@ -59,35 +49,7 @@ export declare class Router {
59
49
  */
60
50
  selectRoute(request: TaskRequest, hostServices: HostServices, interactionMode?: InteractionMode): Promise<RouteSelection>;
61
51
  private selectFromSharedPlan;
62
- private selectFallbackRoute;
63
52
  private buildSelectionFromRoutePlan;
64
- /**
65
- * Hand-written mirror of the shared Rust planner (`rust/inderun-route-core/src/planner.rs`),
66
- * used when the WASM core cannot be loaded. It reproduces the same rejection
67
- * codes and messages so route explanations stay comparable across both paths.
68
- *
69
- * Known, deliberate divergence: this mirror keeps the id-sorted snapshot order
70
- * instead of reproducing the Rust placement/preference ranks, so `optimizeFor`
71
- * does not influence ordering here.
72
- */
73
- private createFallbackPlan;
74
- /**
75
- * Mirrors `infer_failure_code` in the shared Rust planner. The offline class is
76
- * derived from the rejection reasons rather than from the connectivity flag
77
- * alone: an offline host whose only provider is local and non-streaming failed
78
- * on capability, not on connectivity, and reporting `Offline` there would
79
- * contradict the explanation shipped alongside it.
80
- */
81
- private inferFallbackFailureCode;
82
- /**
83
- * Mirrors `evaluate_provider` in the shared Rust planner: it accumulates every
84
- * violated rule rather than short-circuiting, so a rejected provider carries a
85
- * complete explanation. The streaming checks are ordered static-before-dynamic
86
- * for the same reason as in Rust — a provider that never declared streaming
87
- * must not also be reported as dynamically unavailable.
88
- */
89
- private evaluateFallbackCandidate;
90
- private buildFallbackFailureSummary;
91
53
  /**
92
54
  * Routing failure throws before any `route_decided` telemetry is emitted, so the
93
55
  * plan's diagnostics are attached to the exception — that is the only channel
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/core/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAOtD,OAAO,EAGL,KAAK,eAAe,EAEpB,KAAK,YAAY,EAEjB,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,EAE3B,MAAM,oBAAoB,CAAC;AAE5B;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;OAEG;IACH,QAAQ,EAAE,eAAe,CAAC;IAC1B;;OAEG;IACH,iBAAiB,EAAE,eAAe,EAAE,CAAC;IACrC;;OAEG;IACH,SAAS,EAAE,sBAAsB,CAAC;IAClC;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,aAAa,EAAE,MAAM,GAAG,UAAU,CAAC;IACnC;;;OAGG;IACH,wBAAwB,CAAC,EAAE,qBAAqB,CAAC;CAClD;AAED;;;;GAIG;AACH,qBAAa,MAAM;IAEf,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,OAAO;gBADP,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,GAAE,YAAqC;IAGxD;;;;;;;;;;;;;OAaG;IACG,WAAW,CACf,OAAO,EAAE,WAAW,EACpB,YAAY,EAAE,YAAY,EAC1B,eAAe,GAAE,eAAuB,GACvC,OAAO,CAAC,cAAc,CAAC;IAoB1B,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,mBAAmB;IAe3B,OAAO,CAAC,2BAA2B;IA2BnC;;;;;;;;OAQG;IACH,OAAO,CAAC,kBAAkB;IAwE1B;;;;;;OAMG;IACH,OAAO,CAAC,wBAAwB;IAsBhC;;;;;;OAMG;IACH,OAAO,CAAC,yBAAyB;IA4FjC,OAAO,CAAC,2BAA2B;IAyCnC;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;CAkBzB"}
1
+ {"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../../src/core/router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAOtD,OAAO,EAGL,KAAK,eAAe,EAEpB,KAAK,YAAY,EACjB,KAAK,sBAAsB,EAE5B,MAAM,oBAAoB,CAAC;AAE5B;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;OAEG;IACH,QAAQ,EAAE,eAAe,CAAC;IAC1B;;OAEG;IACH,iBAAiB,EAAE,eAAe,EAAE,CAAC;IACrC;;OAEG;IACH,SAAS,EAAE,sBAAsB,CAAC;IAClC;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,qBAAa,MAAM;IAEf,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,OAAO;gBADP,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,GAAE,YAAqC;IAGxD;;;;;;;;;;;;;OAaG;IACG,WAAW,CACf,OAAO,EAAE,WAAW,EACpB,YAAY,EAAE,YAAY,EAC1B,eAAe,GAAE,eAAuB,GACvC,OAAO,CAAC,cAAc,CAAC;IAoB1B,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,2BAA2B;IAuBnC;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;CAkBzB"}
@@ -31,25 +31,24 @@ export class Router {
31
31
  const snapshots = await collectProviderRuntimeSnapshots(this.registry.list(), hostServices);
32
32
  const planInput = buildSharedPlannerInput(request, snapshots, online, interactionMode);
33
33
  const outcome = await this.planner.planRoute(planInput);
34
- if (outcome.routePlan) {
35
- return this.selectFromSharedPlan(snapshots, outcome.routePlan);
34
+ if (!outcome.routePlan) {
35
+ // There is no second planner to degrade to: routing is the shared Rust
36
+ // core's semantics or nothing. Failing here keeps provider selection
37
+ // identical everywhere rather than forking it on a module load failure.
38
+ const reason = outcome.unavailableReason ?? "import_failed";
39
+ throw createInternal(`Route planner unavailable (${reason}).`, {
40
+ details: { plannerUnavailableReason: reason }
41
+ });
36
42
  }
37
- return this.selectFallbackRoute(request, snapshots, online, interactionMode, outcome.unavailableReason);
43
+ return this.selectFromSharedPlan(snapshots, outcome.routePlan);
38
44
  }
39
45
  selectFromSharedPlan(snapshots, routePlan) {
40
46
  if (!routePlan.selectedProviderId) {
41
47
  throw this.routePlanFailure(routePlan);
42
48
  }
43
- return this.buildSelectionFromRoutePlan(snapshots, routePlan, "wasm");
44
- }
45
- selectFallbackRoute(request, snapshots, online, interactionMode, plannerUnavailableReason) {
46
- const plan = this.createFallbackPlan(request, snapshots, online, interactionMode);
47
- if (!plan.selectedProviderId) {
48
- throw this.routePlanFailure(plan);
49
- }
50
- return this.buildSelectionFromRoutePlan(snapshots, plan, "fallback", plannerUnavailableReason);
49
+ return this.buildSelectionFromRoutePlan(snapshots, routePlan);
51
50
  }
52
- buildSelectionFromRoutePlan(snapshots, routePlan, plannerSource, plannerUnavailableReason) {
51
+ buildSelectionFromRoutePlan(snapshots, routePlan) {
53
52
  const orderedProviders = routePlan.candidates
54
53
  .map((candidate) => snapshots.find((snapshot) => snapshot.descriptor.id === candidate.providerId))
55
54
  .filter((snapshot) => snapshot !== undefined);
@@ -61,202 +60,9 @@ export class Router {
61
60
  provider: selected.provider,
62
61
  fallbackProviders: orderedProviders.slice(1).map((snapshot) => snapshot.provider),
63
62
  routePlan,
64
- explanation: routePlan.explanation.summary,
65
- plannerSource,
66
- ...(plannerUnavailableReason ? { plannerUnavailableReason } : {})
63
+ explanation: routePlan.explanation.summary
67
64
  };
68
65
  }
69
- /**
70
- * Hand-written mirror of the shared Rust planner (`rust/inderun-route-core/src/planner.rs`),
71
- * used when the WASM core cannot be loaded. It reproduces the same rejection
72
- * codes and messages so route explanations stay comparable across both paths.
73
- *
74
- * Known, deliberate divergence: this mirror keeps the id-sorted snapshot order
75
- * instead of reproducing the Rust placement/preference ranks, so `optimizeFor`
76
- * does not influence ordering here.
77
- */
78
- createFallbackPlan(request, snapshots, online, interactionMode) {
79
- const planInput = buildSharedPlannerInput(request, snapshots, online, interactionMode);
80
- const eligible = snapshots.filter((candidate) => candidate.descriptor.tasks.includes(planInput.task.kind));
81
- const localCandidates = eligible.filter((candidate) => candidate.descriptor.type !== "cloud");
82
- const cloudCandidates = eligible.filter((candidate) => candidate.descriptor.type === "cloud");
83
- // Constraint filtering must apply to the whole candidate list, not just the selected provider:
84
- // the remainder becomes the engine's fallback chain, and a fallback that violates the request
85
- // constraints would let a `local_required` run be retried against a cloud provider.
86
- const admissible = [];
87
- const rejectedProviders = [];
88
- for (const candidate of snapshots) {
89
- const reasons = this.evaluateFallbackCandidate(candidate, planInput, interactionMode);
90
- if (reasons.length === 0) {
91
- admissible.push(candidate);
92
- }
93
- else {
94
- rejectedProviders.push({ providerId: candidate.descriptor.id, reasons });
95
- }
96
- }
97
- if (admissible.length === 0) {
98
- const failureSummary = this.buildFallbackFailureSummary({
99
- online,
100
- interactionMode,
101
- taskKind: planInput.task.kind,
102
- constraints: planInput.constraints,
103
- localCandidates,
104
- cloudCandidates
105
- });
106
- const failureCode = this.inferFallbackFailureCode(planInput, rejectedProviders);
107
- return {
108
- fallbackProviderIds: [],
109
- candidates: [],
110
- rejectedProviders,
111
- failureCode,
112
- explanation: {
113
- summary: failureSummary
114
- }
115
- };
116
- }
117
- const selectedProviderId = admissible[0]?.descriptor.id;
118
- const summary = interactionMode === "stream"
119
- ? `Selected streaming provider '${selectedProviderId}' deterministically from ${admissible.length} eligible candidate(s).`
120
- : `Selected provider '${selectedProviderId}' deterministically from ${admissible.length} eligible candidate(s).`;
121
- return {
122
- selectedProviderId,
123
- fallbackProviderIds: admissible.slice(1).map((candidate) => candidate.descriptor.id),
124
- candidates: admissible.map((candidate, index) => ({
125
- providerId: candidate.descriptor.id,
126
- order: index
127
- })),
128
- rejectedProviders,
129
- explanation: {
130
- summary,
131
- selectedProviderId
132
- }
133
- };
134
- }
135
- /**
136
- * Mirrors `infer_failure_code` in the shared Rust planner. The offline class is
137
- * derived from the rejection reasons rather than from the connectivity flag
138
- * alone: an offline host whose only provider is local and non-streaming failed
139
- * on capability, not on connectivity, and reporting `Offline` there would
140
- * contradict the explanation shipped alongside it.
141
- */
142
- inferFallbackFailureCode(planInput, rejectedProviders) {
143
- if (rejectedProviders.some((rejected) => rejected.reasons.some((reason) => reason.code === "offline"))) {
144
- return "offline";
145
- }
146
- if (planInput.constraints.cloud === "required" ||
147
- planInput.constraints.privacy === "cloud_required") {
148
- return "unavailable";
149
- }
150
- return "capability_mismatch";
151
- }
152
- /**
153
- * Mirrors `evaluate_provider` in the shared Rust planner: it accumulates every
154
- * violated rule rather than short-circuiting, so a rejected provider carries a
155
- * complete explanation. The streaming checks are ordered static-before-dynamic
156
- * for the same reason as in Rust — a provider that never declared streaming
157
- * must not also be reported as dynamically unavailable.
158
- */
159
- evaluateFallbackCandidate(candidate, planInput, interactionMode) {
160
- const descriptor = candidate.descriptor;
161
- const capabilities = candidate.capabilities;
162
- const constraints = planInput.constraints;
163
- const wantsStream = interactionMode === "stream";
164
- const reasons = [];
165
- // Mirrors `is_data_private` in the shared Rust planner: a provider is private when it
166
- // declares that data does not leave the device, defaulting to non-cloud providers.
167
- const isDataPrivate = descriptor.privacy
168
- ? !descriptor.privacy.dataLeavesDevice
169
- : descriptor.type !== "cloud";
170
- if (!descriptor.tasks.includes(planInput.task.kind)) {
171
- reasons.push({
172
- code: "task_not_supported",
173
- message: `Provider '${descriptor.id}' does not support task '${planInput.task.kind}'.`
174
- });
175
- }
176
- if (!wantsStream && !descriptor.supports.run) {
177
- reasons.push({
178
- code: "run_not_supported",
179
- message: `Provider '${descriptor.id}' does not support run().`
180
- });
181
- }
182
- if (wantsStream) {
183
- if (!descriptor.supports.streaming) {
184
- reasons.push({
185
- code: "streaming_not_supported",
186
- message: `Provider '${descriptor.id}' does not support streaming (Mode 2).`
187
- });
188
- }
189
- else if (capabilities.streamingAvailable === false) {
190
- reasons.push({
191
- code: "streaming_unavailable",
192
- message: capabilities.streamingUnavailableReason ??
193
- `Provider '${descriptor.id}' cannot stream in the current host environment.`
194
- });
195
- }
196
- }
197
- if (constraints.privacy === "local_required" && !isDataPrivate) {
198
- reasons.push({
199
- code: "privacy_constraint",
200
- message: `Provider '${descriptor.id}' does not satisfy local-required privacy.`
201
- });
202
- }
203
- if (constraints.privacy === "cloud_required" && descriptor.type !== "cloud") {
204
- reasons.push({
205
- code: "privacy_constraint",
206
- message: `Provider '${descriptor.id}' does not satisfy cloud-required privacy.`
207
- });
208
- }
209
- if (constraints.cloud === "forbidden" && descriptor.type === "cloud") {
210
- reasons.push({
211
- code: "cloud_constraint",
212
- message: `Provider '${descriptor.id}' is cloud-based but cloud execution is forbidden.`
213
- });
214
- }
215
- if (constraints.cloud === "required" && descriptor.type !== "cloud") {
216
- reasons.push({
217
- code: "cloud_constraint",
218
- message: `Provider '${descriptor.id}' is not cloud-based but cloud execution is required.`
219
- });
220
- }
221
- if (constraints.networkOnline === false && descriptor.type === "cloud") {
222
- reasons.push({
223
- code: "offline",
224
- message: `Provider '${descriptor.id}' requires cloud connectivity, but the host is offline.`
225
- });
226
- }
227
- if (!capabilities.available) {
228
- reasons.push({
229
- code: "capability_unavailable",
230
- message: capabilities.reason ?? `Provider '${descriptor.id}' is currently unavailable.`
231
- });
232
- }
233
- return reasons;
234
- }
235
- buildFallbackFailureSummary(input) {
236
- const wantsCloud = input.constraints.cloud === "required" || input.constraints.privacy === "cloud_required";
237
- const wantsLocal = input.constraints.privacy === "local_required";
238
- if (!input.online && (wantsCloud || input.cloudCandidates.length > 0)) {
239
- return "No network connection is available.";
240
- }
241
- if (wantsCloud) {
242
- if (input.cloudCandidates.length === 0) {
243
- return "No cloud provider found.";
244
- }
245
- return "No cloud provider is currently available.";
246
- }
247
- if (wantsLocal) {
248
- if (input.localCandidates.length === 0) {
249
- return "No on-device provider found.";
250
- }
251
- return "No on-device provider is currently available.";
252
- }
253
- if (input.interactionMode === "stream") {
254
- // Kept verbatim in sync with the shared Rust planner's stream-mode failure
255
- // summary so the message a caller sees does not depend on which planner ran.
256
- return `No provider capable of streaming was found for task '${input.taskKind}'.`;
257
- }
258
- return "No eligible provider found for the current routing constraints.";
259
- }
260
66
  /**
261
67
  * Routing failure throws before any `route_decided` telemetry is emitted, so the
262
68
  * plan's diagnostics are attached to the exception — that is the only channel
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@independo/inderun-web",
3
- "version": "0.3.0-dev.8",
3
+ "version": "0.3.0-dev.9",
4
4
  "description": "TypeScript/Web SDK for IndeRun.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -55,8 +55,8 @@
55
55
  "README.md"
56
56
  ],
57
57
  "dependencies": {
58
- "@independo/inderun-contracts": "^0.3.0-dev.8",
59
- "@independo/inderun-route-core-wasm": "^0.3.0-dev.8"
58
+ "@independo/inderun-contracts": "^0.3.0-dev.9",
59
+ "@independo/inderun-route-core-wasm": "^0.3.0-dev.9"
60
60
  },
61
61
  "peerDependencies": {
62
62
  "@huggingface/transformers": "^4.0.0"