@independo/inderun-web 0.2.2 → 0.3.0-dev.10

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.
@@ -2,6 +2,11 @@ import type { RoutePlan as SharedPlannerRoutePlan, RoutePlannerInput as SharedPl
2
2
  import type { HostServices } from "./host.js";
3
3
  import type { ProviderAdapter } from "./provider.js";
4
4
  export type { SharedPlannerInput, SharedPlannerRoutePlan };
5
+ /**
6
+ * Interaction mode the caller is requesting a route for. Passed to the shared
7
+ * planner, which rejects providers that cannot satisfy it.
8
+ */
9
+ export type InteractionMode = NonNullable<SharedPlannerInput["interactionMode"]>;
5
10
  /**
6
11
  * Minimal shape of the shared route-core module (the WASM wrapper), used to
7
12
  * plan routes via JSON in/JSON out. `initSharedCore` initializes the WASM
@@ -12,25 +17,23 @@ export interface SharedPlannerModule {
12
17
  planRouteJson: (inputJson: string) => string | Promise<string>;
13
18
  }
14
19
  /**
15
- * Coarse, privacy-safe reason the WASM planner produced no route plan. Used
16
- * only for observability (telemetry/logging) — callers still fall back to
17
- * 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.
18
23
  */
19
24
  export type WasmUnavailableReason = "import_failed" | "invalid_module_shape" | "init_failed" | "plan_failed";
20
25
  /**
21
26
  * Result of a {@link RoutePlanner} attempt. `routePlan` is `null` when the
22
- * planner could not produce a plan, in which case `source` is `"unavailable"`
23
- * and `unavailableReason` narrows down why, so callers can log/emit telemetry
24
- * instead of silently swapping planners.
27
+ * planner could not produce a plan, and `unavailableReason` then narrows down
28
+ * why.
25
29
  */
26
30
  export interface PlannerOutcome {
27
31
  routePlan: SharedPlannerRoutePlan | null;
28
- source: "wasm" | "unavailable";
29
32
  unavailableReason?: WasmUnavailableReason;
30
33
  }
31
34
  /**
32
35
  * Strategy that turns planner input into a deterministic route plan, or an
33
- * "unavailable" outcome (callers then fall back to local selection).
36
+ * "unavailable" outcome (which the router reports as a routing failure).
34
37
  */
35
38
  export interface RoutePlanner {
36
39
  planRoute(input: SharedPlannerInput): Promise<PlannerOutcome>;
@@ -50,13 +53,14 @@ export interface ProviderRuntimeSnapshot {
50
53
  * Vite/webpack/Rollup can statically resolve and chunk it — a variable
51
54
  * specifier defeats bundler analysis and silently never loads in the browser
52
55
  * (see issue #109). The module is imported lazily and memoized; if it cannot
53
- * be loaded, `planRoute` resolves an "unavailable" outcome so the engine can
54
- * fall back to local selection, and logs once via `console.warn` so the
55
- * 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.
56
60
  */
57
61
  export declare class WasmRoutePlanner implements RoutePlanner {
58
62
  private modulePromise?;
59
- private loadFailureReason?;
63
+ private loadFailure?;
60
64
  private hasWarned;
61
65
  planRoute(input: SharedPlannerInput): Promise<PlannerOutcome>;
62
66
  private loadModule;
@@ -65,7 +69,11 @@ export declare class WasmRoutePlanner implements RoutePlanner {
65
69
  }
66
70
  /**
67
71
  * Describes each provider and evaluates its dynamic capabilities against the
68
- * 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.
69
77
  */
70
78
  export declare function collectProviderRuntimeSnapshots(registryProviders: ProviderAdapter[], hostServices: HostServices): Promise<ProviderRuntimeSnapshot[]>;
71
79
  /**
@@ -73,5 +81,5 @@ export declare function collectProviderRuntimeSnapshots(registryProviders: Provi
73
81
  * consumes, applying default constraints/preferences and the current network
74
82
  * state.
75
83
  */
76
- export declare function buildSharedPlannerInput(request: TaskRequest, snapshots: ProviderRuntimeSnapshot[], networkOnline: boolean): SharedPlannerInput;
84
+ export declare function buildSharedPlannerInput(request: TaskRequest, snapshots: ProviderRuntimeSnapshot[], networkOnline: boolean, interactionMode?: InteractionMode): SharedPlannerInput;
77
85
  //# sourceMappingURL=route-planner.d.ts.map
@@ -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;;;;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,GACrB,kBAAkB,CAsBpB"}
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,37 +67,41 @@ 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 {
82
86
  provider,
83
87
  descriptor: toSharedPlannerDescriptor(descriptor),
84
- capabilities: toSharedPlannerCapabilities(capabilities)
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
91
94
  * consumes, applying default constraints/preferences and the current network
92
95
  * state.
93
96
  */
94
- export function buildSharedPlannerInput(request, snapshots, networkOnline) {
97
+ export function buildSharedPlannerInput(request, snapshots, networkOnline, interactionMode = "run") {
95
98
  const constraints = request.constraints ?? {};
96
99
  const preferences = request.preferences ?? {};
97
100
  return {
98
101
  task: {
99
102
  kind: request.task.kind
100
103
  },
104
+ interactionMode,
101
105
  constraints: {
102
106
  privacy: constraints.privacy ?? "cloud_allowed",
103
107
  cloud: constraints.cloud ?? "allowed",
@@ -118,8 +122,10 @@ function toSharedPlannerDescriptor(descriptor) {
118
122
  id: descriptor.id,
119
123
  type: descriptor.type,
120
124
  supports: {
121
- run: descriptor.supports.run
125
+ run: descriptor.supports.run,
126
+ streaming: descriptor.supports.streaming
122
127
  },
128
+ cancel: descriptor.cancel,
123
129
  tasks: descriptor.tasks,
124
130
  ...(descriptor.privacy
125
131
  ? {
@@ -131,9 +137,30 @@ function toSharedPlannerDescriptor(descriptor) {
131
137
  : {})
132
138
  };
133
139
  }
134
- function toSharedPlannerCapabilities(capabilities) {
140
+ /**
141
+ * Projects the dynamic capability snapshot for the planner.
142
+ *
143
+ * `stream()` being absent from the adapter is folded in here rather than
144
+ * filtered out downstream: an adapter that declares `supports.streaming` but
145
+ * never implemented the method is not streaming-capable, and saying so as a
146
+ * planner-visible reason is what lets the route explanation name the provider
147
+ * instead of silently dropping it.
148
+ */
149
+ function toSharedPlannerCapabilities(capabilities, descriptor, provider) {
150
+ const declaresStreaming = capabilities.streamingAvailable ?? descriptor.supports.streaming;
151
+ const implementsStream = typeof provider.stream === "function";
152
+ const streamingAvailable = declaresStreaming && implementsStream;
153
+ const streamingUnavailableReason = capabilities.streamingUnavailableReason ??
154
+ (declaresStreaming && !implementsStream
155
+ ? `Provider '${descriptor.id}' declares streaming but does not implement stream().`
156
+ : undefined);
135
157
  return {
136
158
  available: capabilities.available,
137
- ...(capabilities.reason ? { reason: capabilities.reason } : {})
159
+ ...(capabilities.reason ? { reason: capabilities.reason } : {}),
160
+ streamingAvailable,
161
+ ...(!streamingAvailable && streamingUnavailableReason ? { streamingUnavailableReason } : {}),
162
+ ...(capabilities.cancellationAvailable !== undefined
163
+ ? { cancellationAvailable: capabilities.cancellationAvailable }
164
+ : {})
138
165
  };
139
166
  }
@@ -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 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,
@@ -52,13 +42,19 @@ export declare class Router {
52
42
  * - `CapabilityMismatch` when `execution === 'on_device'` but no local provider is registered or available.
53
43
  * - `Offline` when `execution === 'cloud'` but the device lacks internet connectivity.
54
44
  * - `Unavailable` when `execution === 'cloud'` but no cloud provider is registered or functional.
45
+ *
46
+ * The requested `interactionMode` is a planning input, not a post-filter: the
47
+ * shared planner rejects providers that cannot satisfy it and says why, so the
48
+ * whole returned chain is mode-compatible.
55
49
  */
56
- selectRoute(request: TaskRequest, hostServices: HostServices): Promise<RouteSelection>;
50
+ selectRoute(request: TaskRequest, hostServices: HostServices, interactionMode?: InteractionMode): Promise<RouteSelection>;
57
51
  private selectFromSharedPlan;
58
- private selectFallbackRoute;
59
52
  private buildSelectionFromRoutePlan;
60
- private createFallbackPlan;
61
- private buildFallbackFailureSummary;
53
+ /**
54
+ * Routing failure throws before any `route_decided` telemetry is emitted, so the
55
+ * plan's diagnostics are attached to the exception — that is the only channel
56
+ * through which a caller learns *why* each provider was rejected.
57
+ */
62
58
  private routePlanFailure;
63
59
  }
64
60
  //# sourceMappingURL=router.d.ts.map
@@ -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,EAIL,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;;;;;;;;;OASG;IACG,WAAW,CAAC,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,GAAG,OAAO,CAAC,cAAc,CAAC;IAc5F,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,mBAAmB;IAc3B,OAAO,CAAC,2BAA2B;IA2BnC,OAAO,CAAC,kBAAkB;IA0F1B,OAAO,CAAC,2BAA2B;IAiCnC,OAAO,CAAC,gBAAgB;CAYzB"}
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"}
@@ -21,31 +21,34 @@ export class Router {
21
21
  * - `CapabilityMismatch` when `execution === 'on_device'` but no local provider is registered or available.
22
22
  * - `Offline` when `execution === 'cloud'` but the device lacks internet connectivity.
23
23
  * - `Unavailable` when `execution === 'cloud'` but no cloud provider is registered or functional.
24
+ *
25
+ * The requested `interactionMode` is a planning input, not a post-filter: the
26
+ * shared planner rejects providers that cannot satisfy it and says why, so the
27
+ * whole returned chain is mode-compatible.
24
28
  */
25
- async selectRoute(request, hostServices) {
29
+ async selectRoute(request, hostServices, interactionMode = "run") {
26
30
  const online = await hostServices.connectivity.isOnline();
27
31
  const snapshots = await collectProviderRuntimeSnapshots(this.registry.list(), hostServices);
28
- const planInput = buildSharedPlannerInput(request, snapshots, online);
32
+ const planInput = buildSharedPlannerInput(request, snapshots, online, interactionMode);
29
33
  const outcome = await this.planner.planRoute(planInput);
30
- if (outcome.routePlan) {
31
- 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
+ });
32
42
  }
33
- return this.selectFallbackRoute(request, snapshots, online, outcome.unavailableReason);
43
+ return this.selectFromSharedPlan(snapshots, outcome.routePlan);
34
44
  }
35
45
  selectFromSharedPlan(snapshots, routePlan) {
36
46
  if (!routePlan.selectedProviderId) {
37
47
  throw this.routePlanFailure(routePlan);
38
48
  }
39
- return this.buildSelectionFromRoutePlan(snapshots, routePlan, "wasm");
40
- }
41
- selectFallbackRoute(request, snapshots, online, plannerUnavailableReason) {
42
- const plan = this.createFallbackPlan(request, snapshots, online);
43
- if (!plan.selectedProviderId) {
44
- throw this.routePlanFailure(plan);
45
- }
46
- return this.buildSelectionFromRoutePlan(snapshots, plan, "fallback", plannerUnavailableReason);
49
+ return this.buildSelectionFromRoutePlan(snapshots, routePlan);
47
50
  }
48
- buildSelectionFromRoutePlan(snapshots, routePlan, plannerSource, plannerUnavailableReason) {
51
+ buildSelectionFromRoutePlan(snapshots, routePlan) {
49
52
  const orderedProviders = routePlan.candidates
50
53
  .map((candidate) => snapshots.find((snapshot) => snapshot.descriptor.id === candidate.providerId))
51
54
  .filter((snapshot) => snapshot !== undefined);
@@ -57,114 +60,30 @@ export class Router {
57
60
  provider: selected.provider,
58
61
  fallbackProviders: orderedProviders.slice(1).map((snapshot) => snapshot.provider),
59
62
  routePlan,
60
- explanation: routePlan.explanation.summary,
61
- plannerSource,
62
- ...(plannerUnavailableReason ? { plannerUnavailableReason } : {})
63
- };
64
- }
65
- createFallbackPlan(request, snapshots, online) {
66
- const planInput = buildSharedPlannerInput(request, snapshots, online);
67
- const eligible = snapshots
68
- .filter((candidate) => candidate.descriptor.tasks.includes(planInput.task.kind))
69
- .filter((candidate) => candidate.descriptor.supports.run);
70
- const localCandidates = eligible.filter((candidate) => candidate.descriptor.type !== "cloud");
71
- const cloudCandidates = eligible.filter((candidate) => candidate.descriptor.type === "cloud");
72
- // Constraint filtering must apply to the whole candidate list, not just the selected provider:
73
- // the remainder becomes the engine's fallback chain, and a fallback that violates the request
74
- // constraints would let a `local_required` run be retried against a cloud provider.
75
- const admissible = eligible.filter((candidate) => {
76
- const descriptor = candidate.descriptor;
77
- const constraints = planInput.constraints;
78
- // Mirrors `is_data_private` in the shared Rust planner: a provider is private when it
79
- // declares that data does not leave the device, defaulting to non-cloud providers.
80
- const isDataPrivate = descriptor.privacy
81
- ? !descriptor.privacy.dataLeavesDevice
82
- : descriptor.type !== "cloud";
83
- if (constraints.cloud === "forbidden" && descriptor.type === "cloud") {
84
- return false;
85
- }
86
- if (constraints.cloud === "required" && descriptor.type !== "cloud") {
87
- return false;
88
- }
89
- if (constraints.privacy === "local_required" && !isDataPrivate) {
90
- return false;
91
- }
92
- if (constraints.privacy === "cloud_required" && descriptor.type !== "cloud") {
93
- return false;
94
- }
95
- if (!online && descriptor.type === "cloud") {
96
- return false;
97
- }
98
- return candidate.capabilities.available;
99
- });
100
- if (admissible.length === 0) {
101
- const failureSummary = this.buildFallbackFailureSummary({
102
- online,
103
- constraints: planInput.constraints,
104
- localCandidates,
105
- cloudCandidates
106
- });
107
- const failureCode = !online
108
- ? "offline"
109
- : planInput.constraints.cloud === "required" ||
110
- planInput.constraints.privacy === "cloud_required"
111
- ? "unavailable"
112
- : "capability_mismatch";
113
- return {
114
- fallbackProviderIds: [],
115
- candidates: [],
116
- rejectedProviders: [],
117
- failureCode,
118
- explanation: {
119
- summary: failureSummary
120
- }
121
- };
122
- }
123
- const selectedProviderId = admissible[0]?.descriptor.id;
124
- return {
125
- selectedProviderId,
126
- fallbackProviderIds: admissible.slice(1).map((candidate) => candidate.descriptor.id),
127
- candidates: admissible.map((candidate, index) => ({
128
- providerId: candidate.descriptor.id,
129
- order: index
130
- })),
131
- rejectedProviders: [],
132
- explanation: {
133
- summary: `Selected provider '${selectedProviderId}' deterministically from ${admissible.length} eligible candidate(s).`,
134
- selectedProviderId
135
- }
63
+ explanation: routePlan.explanation.summary
136
64
  };
137
65
  }
138
- buildFallbackFailureSummary(input) {
139
- const wantsCloud = input.constraints.cloud === "required" || input.constraints.privacy === "cloud_required";
140
- const wantsLocal = input.constraints.privacy === "local_required";
141
- if (!input.online && (wantsCloud || input.cloudCandidates.length > 0)) {
142
- return "No network connection is available.";
143
- }
144
- if (wantsCloud) {
145
- if (input.cloudCandidates.length === 0) {
146
- return "No cloud provider found.";
147
- }
148
- return "No cloud provider is currently available.";
149
- }
150
- if (wantsLocal) {
151
- if (input.localCandidates.length === 0) {
152
- return "No on-device provider found.";
153
- }
154
- return "No on-device provider is currently available.";
155
- }
156
- return "No eligible provider found for the current routing constraints.";
157
- }
66
+ /**
67
+ * Routing failure throws before any `route_decided` telemetry is emitted, so the
68
+ * plan's diagnostics are attached to the exception — that is the only channel
69
+ * through which a caller learns *why* each provider was rejected.
70
+ */
158
71
  routePlanFailure(routePlan) {
159
72
  const message = routePlan.explanation.summary;
73
+ const details = {
74
+ details: {
75
+ failureCode: routePlan.failureCode,
76
+ rejectedProviders: routePlan.rejectedProviders
77
+ }
78
+ };
160
79
  switch (routePlan.failureCode) {
161
80
  case "offline":
162
- throw createOffline(message);
81
+ throw createOffline(message, details);
163
82
  case "unavailable":
164
- throw createUnavailable(message);
83
+ throw createUnavailable(message, details);
165
84
  case "capability_mismatch":
166
85
  default:
167
- throw createCapabilityMismatch(message);
86
+ throw createCapabilityMismatch(message, details);
168
87
  }
169
88
  }
170
89
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * A single dispatched server-sent event.
3
+ *
4
+ * `event` and `id` are `undefined` when the stream did not carry those fields.
5
+ * `data` is the accumulated data payload with its trailing newline removed, and
6
+ * is passed through verbatim — sentinels such as OpenAI's `[DONE]` are the
7
+ * caller's concern, not the framing layer's.
8
+ */
9
+ export interface SseEvent {
10
+ event?: string;
11
+ data: string;
12
+ id?: string;
13
+ }
14
+ /**
15
+ * Frames a raw byte stream into server-sent events.
16
+ *
17
+ * Follows the WHATWG server-sent events stream parsing rules, minus
18
+ * reconnection: `retry` is parsed as a field and ignored, and there is no
19
+ * Last-Event-ID reconnect logic. Concretely that means:
20
+ *
21
+ * - lines end with LF, CRLF, or bare CR;
22
+ * - a line beginning with `:` is a comment (servers send these as keep-alives);
23
+ * - `field: value` strips exactly one leading space from the value, and a line
24
+ * with no colon is that field with an empty value;
25
+ * - `data` fields accumulate, joined by newlines;
26
+ * - a blank line dispatches the accumulated event, or resets without
27
+ * dispatching if no data was accumulated;
28
+ * - a stream that ends mid-event does **not** dispatch the partial event, since
29
+ * a truncated event must never be reported as a complete one.
30
+ *
31
+ * Chunk boundaries are meaningless: this decodes incrementally (`stream: true`)
32
+ * so a boundary may fall inside a multi-byte UTF-8 sequence or mid-line.
33
+ *
34
+ * Held to `contracts/fixtures/streaming/sse-framing.json` together with the
35
+ * Swift and Kotlin implementations of the same protocol.
36
+ *
37
+ * @param chunks - Raw response body chunks, in arrival order.
38
+ */
39
+ export declare function parseSseStream(chunks: AsyncIterable<Uint8Array>): AsyncGenerator<SseEvent>;
40
+ //# sourceMappingURL=sse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sse.d.ts","sourceRoot":"","sources":["../../src/core/sse.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,CAAC,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAuB,cAAc,CAAC,MAAM,EAAE,aAAa,CAAC,UAAU,CAAC,GAAG,cAAc,CAAC,QAAQ,CAAC,CAsFjG"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Frames a raw byte stream into server-sent events.
3
+ *
4
+ * Follows the WHATWG server-sent events stream parsing rules, minus
5
+ * reconnection: `retry` is parsed as a field and ignored, and there is no
6
+ * Last-Event-ID reconnect logic. Concretely that means:
7
+ *
8
+ * - lines end with LF, CRLF, or bare CR;
9
+ * - a line beginning with `:` is a comment (servers send these as keep-alives);
10
+ * - `field: value` strips exactly one leading space from the value, and a line
11
+ * with no colon is that field with an empty value;
12
+ * - `data` fields accumulate, joined by newlines;
13
+ * - a blank line dispatches the accumulated event, or resets without
14
+ * dispatching if no data was accumulated;
15
+ * - a stream that ends mid-event does **not** dispatch the partial event, since
16
+ * a truncated event must never be reported as a complete one.
17
+ *
18
+ * Chunk boundaries are meaningless: this decodes incrementally (`stream: true`)
19
+ * so a boundary may fall inside a multi-byte UTF-8 sequence or mid-line.
20
+ *
21
+ * Held to `contracts/fixtures/streaming/sse-framing.json` together with the
22
+ * Swift and Kotlin implementations of the same protocol.
23
+ *
24
+ * @param chunks - Raw response body chunks, in arrival order.
25
+ */
26
+ export async function* parseSseStream(chunks) {
27
+ const decoder = new TextDecoder("utf-8");
28
+ let buffer = "";
29
+ let eventType;
30
+ let data;
31
+ let id;
32
+ const takeEvent = () => {
33
+ const dispatched = data === undefined ? undefined : buildEvent(eventType, data, id);
34
+ eventType = undefined;
35
+ data = undefined;
36
+ id = undefined;
37
+ return dispatched;
38
+ };
39
+ const handleLine = (line) => {
40
+ if (line.length === 0)
41
+ return takeEvent();
42
+ if (line.startsWith(":"))
43
+ return undefined;
44
+ const colon = line.indexOf(":");
45
+ const field = colon === -1 ? line : line.slice(0, colon);
46
+ let value = colon === -1 ? "" : line.slice(colon + 1);
47
+ if (value.startsWith(" "))
48
+ value = value.slice(1);
49
+ switch (field) {
50
+ case "event":
51
+ eventType = value;
52
+ break;
53
+ case "data":
54
+ data = data === undefined ? value : `${data}\n${value}`;
55
+ break;
56
+ case "id":
57
+ // Per the SSE spec an id containing a NUL is ignored outright.
58
+ if (!value.includes("\0"))
59
+ id = value;
60
+ break;
61
+ default:
62
+ // `retry` and anything unrecognized are dropped rather than erroring.
63
+ break;
64
+ }
65
+ return undefined;
66
+ };
67
+ for await (const chunk of chunks) {
68
+ buffer += decoder.decode(chunk, { stream: true });
69
+ for (;;) {
70
+ const line = takeLine();
71
+ if (line === undefined)
72
+ break;
73
+ const dispatched = handleLine(line);
74
+ if (dispatched)
75
+ yield dispatched;
76
+ }
77
+ }
78
+ // A trailing bare CR is ambiguous until the stream ends: it could still have
79
+ // been the first half of a CRLF.
80
+ buffer += decoder.decode();
81
+ for (;;) {
82
+ const line = takeLine(true);
83
+ if (line === undefined)
84
+ break;
85
+ const dispatched = handleLine(line);
86
+ if (dispatched)
87
+ yield dispatched;
88
+ }
89
+ function takeLine(atEnd = false) {
90
+ for (let index = 0; index < buffer.length; index += 1) {
91
+ const char = buffer[index];
92
+ if (char === "\n") {
93
+ const line = buffer.slice(0, index);
94
+ buffer = buffer.slice(index + 1);
95
+ return line;
96
+ }
97
+ if (char === "\r") {
98
+ const isLast = index === buffer.length - 1;
99
+ // Withhold a trailing CR until we know whether an LF follows, unless
100
+ // the stream is over and no LF can arrive.
101
+ if (isLast && !atEnd)
102
+ return undefined;
103
+ const skip = buffer[index + 1] === "\n" ? 2 : 1;
104
+ const line = buffer.slice(0, index);
105
+ buffer = buffer.slice(index + skip);
106
+ return line;
107
+ }
108
+ }
109
+ return undefined;
110
+ }
111
+ }
112
+ function buildEvent(eventType, data, id) {
113
+ return {
114
+ ...(eventType !== undefined ? { event: eventType } : {}),
115
+ data,
116
+ ...(id !== undefined ? { id } : {})
117
+ };
118
+ }