@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.
- package/README.md +39 -0
- package/dist/core/browser-host.d.ts +29 -2
- package/dist/core/browser-host.d.ts.map +1 -1
- package/dist/core/browser-host.js +157 -0
- package/dist/core/engine.d.ts +37 -1
- package/dist/core/engine.d.ts.map +1 -1
- package/dist/core/engine.js +294 -25
- package/dist/core/event-gate.d.ts +37 -0
- package/dist/core/event-gate.d.ts.map +1 -0
- package/dist/core/event-gate.js +63 -0
- package/dist/core/generated/inderun-api.d.ts +5 -1
- package/dist/core/generated/inderun-api.d.ts.map +1 -1
- package/dist/core/host.d.ts +1 -1
- package/dist/core/host.d.ts.map +1 -1
- package/dist/core/provider.d.ts +81 -1
- package/dist/core/provider.d.ts.map +1 -1
- package/dist/core/route-planner.d.ts +22 -14
- package/dist/core/route-planner.d.ts.map +1 -1
- package/dist/core/route-planner.js +51 -24
- package/dist/core/router.d.ts +11 -15
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +32 -113
- package/dist/core/sse.d.ts +40 -0
- package/dist/core/sse.d.ts.map +1 -0
- package/dist/core/sse.js +118 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/providers/openai/provider.d.ts +44 -1
- package/dist/providers/openai/provider.d.ts.map +1 -1
- package/dist/providers/openai/provider.js +192 -30
- package/package.json +4 -4
|
@@ -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.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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,
|
|
23
|
-
*
|
|
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 (
|
|
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
|
|
54
|
-
*
|
|
55
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
-
|
|
14
|
+
loadFailure;
|
|
14
15
|
hasWarned = false;
|
|
15
16
|
async planRoute(input) {
|
|
16
17
|
const module = await this.loadModule();
|
|
17
18
|
if (!module) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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)
|
|
28
|
+
return { routePlan: JSON.parse(result) };
|
|
26
29
|
}
|
|
27
30
|
catch (error) {
|
|
28
31
|
this.warnOnce("plan_failed", error);
|
|
29
|
-
return { routePlan: null,
|
|
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.
|
|
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.
|
|
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.
|
|
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});
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/core/router.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
61
|
-
|
|
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,
|
|
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"}
|
package/dist/core/router.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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"}
|
package/dist/core/sse.js
ADDED
|
@@ -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
|
+
}
|