@independo/inderun-web 0.3.0-dev.4 → 0.3.0-dev.6

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,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, type WasmUnavailableReason } from "./route-planner.js";
6
6
  /**
7
7
  * Output structure from the Router containing the chosen provider and a route selection description.
8
8
  */
@@ -52,13 +52,47 @@ export declare class Router {
52
52
  * - `CapabilityMismatch` when `execution === 'on_device'` but no local provider is registered or available.
53
53
  * - `Offline` when `execution === 'cloud'` but the device lacks internet connectivity.
54
54
  * - `Unavailable` when `execution === 'cloud'` but no cloud provider is registered or functional.
55
+ *
56
+ * The requested `interactionMode` is a planning input, not a post-filter: the
57
+ * shared planner rejects providers that cannot satisfy it and says why, so the
58
+ * whole returned chain is mode-compatible.
55
59
  */
56
- selectRoute(request: TaskRequest, hostServices: HostServices): Promise<RouteSelection>;
60
+ selectRoute(request: TaskRequest, hostServices: HostServices, interactionMode?: InteractionMode): Promise<RouteSelection>;
57
61
  private selectFromSharedPlan;
58
62
  private selectFallbackRoute;
59
63
  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
+ */
60
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;
61
90
  private buildFallbackFailureSummary;
91
+ /**
92
+ * Routing failure throws before any `route_decided` telemetry is emitted, so the
93
+ * plan's diagnostics are attached to the exception — that is the only channel
94
+ * through which a caller learns *why* each provider was rejected.
95
+ */
62
96
  private routePlanFailure;
63
97
  }
64
98
  //# 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,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"}
@@ -21,16 +21,20 @@ 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
34
  if (outcome.routePlan) {
31
35
  return this.selectFromSharedPlan(snapshots, outcome.routePlan);
32
36
  }
33
- return this.selectFallbackRoute(request, snapshots, online, outcome.unavailableReason);
37
+ return this.selectFallbackRoute(request, snapshots, online, interactionMode, outcome.unavailableReason);
34
38
  }
35
39
  selectFromSharedPlan(snapshots, routePlan) {
36
40
  if (!routePlan.selectedProviderId) {
@@ -38,8 +42,8 @@ export class Router {
38
42
  }
39
43
  return this.buildSelectionFromRoutePlan(snapshots, routePlan, "wasm");
40
44
  }
41
- selectFallbackRoute(request, snapshots, online, plannerUnavailableReason) {
42
- const plan = this.createFallbackPlan(request, snapshots, online);
45
+ selectFallbackRoute(request, snapshots, online, interactionMode, plannerUnavailableReason) {
46
+ const plan = this.createFallbackPlan(request, snapshots, online, interactionMode);
43
47
  if (!plan.selectedProviderId) {
44
48
  throw this.routePlanFailure(plan);
45
49
  }
@@ -62,58 +66,48 @@ export class Router {
62
66
  ...(plannerUnavailableReason ? { plannerUnavailableReason } : {})
63
67
  };
64
68
  }
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);
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));
70
81
  const localCandidates = eligible.filter((candidate) => candidate.descriptor.type !== "cloud");
71
82
  const cloudCandidates = eligible.filter((candidate) => candidate.descriptor.type === "cloud");
72
83
  // Constraint filtering must apply to the whole candidate list, not just the selected provider:
73
84
  // the remainder becomes the engine's fallback chain, and a fallback that violates the request
74
85
  // 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;
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);
94
92
  }
95
- if (!online && descriptor.type === "cloud") {
96
- return false;
93
+ else {
94
+ rejectedProviders.push({ providerId: candidate.descriptor.id, reasons });
97
95
  }
98
- return candidate.capabilities.available;
99
- });
96
+ }
100
97
  if (admissible.length === 0) {
101
98
  const failureSummary = this.buildFallbackFailureSummary({
102
99
  online,
100
+ interactionMode,
101
+ taskKind: planInput.task.kind,
103
102
  constraints: planInput.constraints,
104
103
  localCandidates,
105
104
  cloudCandidates
106
105
  });
107
- const failureCode = !online
108
- ? "offline"
109
- : planInput.constraints.cloud === "required" ||
110
- planInput.constraints.privacy === "cloud_required"
111
- ? "unavailable"
112
- : "capability_mismatch";
106
+ const failureCode = this.inferFallbackFailureCode(planInput, rejectedProviders);
113
107
  return {
114
108
  fallbackProviderIds: [],
115
109
  candidates: [],
116
- rejectedProviders: [],
110
+ rejectedProviders,
117
111
  failureCode,
118
112
  explanation: {
119
113
  summary: failureSummary
@@ -121,6 +115,9 @@ export class Router {
121
115
  };
122
116
  }
123
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).`;
124
121
  return {
125
122
  selectedProviderId,
126
123
  fallbackProviderIds: admissible.slice(1).map((candidate) => candidate.descriptor.id),
@@ -128,13 +125,113 @@ export class Router {
128
125
  providerId: candidate.descriptor.id,
129
126
  order: index
130
127
  })),
131
- rejectedProviders: [],
128
+ rejectedProviders,
132
129
  explanation: {
133
- summary: `Selected provider '${selectedProviderId}' deterministically from ${admissible.length} eligible candidate(s).`,
130
+ summary,
134
131
  selectedProviderId
135
132
  }
136
133
  };
137
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
+ }
138
235
  buildFallbackFailureSummary(input) {
139
236
  const wantsCloud = input.constraints.cloud === "required" || input.constraints.privacy === "cloud_required";
140
237
  const wantsLocal = input.constraints.privacy === "local_required";
@@ -153,18 +250,34 @@ export class Router {
153
250
  }
154
251
  return "No on-device provider is currently available.";
155
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
+ }
156
258
  return "No eligible provider found for the current routing constraints.";
157
259
  }
260
+ /**
261
+ * Routing failure throws before any `route_decided` telemetry is emitted, so the
262
+ * plan's diagnostics are attached to the exception — that is the only channel
263
+ * through which a caller learns *why* each provider was rejected.
264
+ */
158
265
  routePlanFailure(routePlan) {
159
266
  const message = routePlan.explanation.summary;
267
+ const details = {
268
+ details: {
269
+ failureCode: routePlan.failureCode,
270
+ rejectedProviders: routePlan.rejectedProviders
271
+ }
272
+ };
160
273
  switch (routePlan.failureCode) {
161
274
  case "offline":
162
- throw createOffline(message);
275
+ throw createOffline(message, details);
163
276
  case "unavailable":
164
- throw createUnavailable(message);
277
+ throw createUnavailable(message, details);
165
278
  case "capability_mismatch":
166
279
  default:
167
- throw createCapabilityMismatch(message);
280
+ throw createCapabilityMismatch(message, details);
168
281
  }
169
282
  }
170
283
  }
@@ -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
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { type ConnectivityService, type DeviceConstraintsService, type SecureStorageService, type ClockService, type HttpClientService, type HttpRequest, type HttpResponse, type HostServices, type ThermalState } from "./core/host.js";
2
- export { type ProviderDescriptor, type ProviderDynamicCapabilities, type ProviderCapabilitySnapshot, type RunContext, type ProviderAdapter, type ProviderStreamContext, type ProviderStreamEvent } from "./core/provider.js";
1
+ export { type AbortSignalLike, type ConnectivityService, type DeviceConstraintsService, type SecureStorageService, type ClockService, type HttpClientService, type HttpRequest, type HttpResponse, type HttpStreamResponse, type HttpStreamingClientService, type HostServices, type ThermalState } from "./core/host.js";
2
+ export { type ProviderDescriptor, type ProviderDynamicCapabilities, type ProviderCapabilitySnapshot, type RunContext, type ProviderAdapter, type ProviderStreamContext, type ProviderStreamEvent, type StreamRun } from "./core/provider.js";
3
3
  export { EventGate } from "./core/event-gate.js";
4
4
  export { ProviderRegistry } from "./core/registry.js";
5
5
  export { type RouteSelection, Router } from "./core/router.js";
@@ -8,6 +8,6 @@ export { type IndeRunExceptionParams, IndeRunException, createCapabilityMismatch
8
8
  export { IndeRun, type StreamHandleResult } from "./core/engine.js";
9
9
  export type { IndeRunApi } from "./core/generated/inderun-api.js";
10
10
  export { type TelemetryEventType, type TelemetryEvent, type TelemetryService, NoOpTelemetryService } from "./core/telemetry.js";
11
- export { BrowserConnectivityService, FetchHttpClient, SystemClockService, createBrowserHostServices, type CreateBrowserHostServicesOptions, type FetchHttpClientOptions } from "./core/browser-host.js";
11
+ export { BrowserConnectivityService, FetchHttpClient, FetchStreamingHttpClient, SystemClockService, createBrowserHostServices, type CreateBrowserHostServicesOptions, type FetchHttpClientOptions } from "./core/browser-host.js";
12
12
  export { createIndeRunWeb, type CreateIndeRunWebOptions } from "./web-sdk.js";
13
13
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,EAC/B,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACzB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,gBAAgB,EACjB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,KAAK,sBAAsB,EAC3B,gBAAgB,EAChB,wBAAwB,EACxB,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,kBAAkB,EACnB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACpE,YAAY,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAElE,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,oBAAoB,EACrB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,eAAe,EACf,kBAAkB,EAClB,yBAAyB,EACzB,KAAK,gCAAgC,EACrC,KAAK,sBAAsB,EAC5B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,0BAA0B,EAC/B,KAAK,YAAY,EACjB,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,2BAA2B,EAChC,KAAK,0BAA0B,EAC/B,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,qBAAqB,EAC1B,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACf,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAEtD,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAC1B,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,gBAAgB,EACjB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,KAAK,sBAAsB,EAC3B,gBAAgB,EAChB,wBAAwB,EACxB,aAAa,EACb,eAAe,EACf,iBAAiB,EACjB,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,kBAAkB,EACnB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,OAAO,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACpE,YAAY,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAElE,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,oBAAoB,EACrB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,0BAA0B,EAC1B,eAAe,EACf,wBAAwB,EACxB,kBAAkB,EAClB,yBAAyB,EACzB,KAAK,gCAAgC,EACrC,KAAK,sBAAsB,EAC5B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ export { WasmRoutePlanner } from "./core/route-planner.js";
7
7
  export { IndeRunException, createCapabilityMismatch, createOffline, createAuthError, createRateLimited, createTimeout, createUnavailable, createInternal, toIndeRunException } from "./core/errors.js";
8
8
  export { IndeRun } from "./core/engine.js";
9
9
  export { NoOpTelemetryService } from "./core/telemetry.js";
10
- export { BrowserConnectivityService, FetchHttpClient, SystemClockService, createBrowserHostServices } from "./core/browser-host.js";
10
+ export { BrowserConnectivityService, FetchHttpClient, FetchStreamingHttpClient, SystemClockService, createBrowserHostServices } from "./core/browser-host.js";
11
11
  export { createIndeRunWeb } from "./web-sdk.js";
12
12
  // The OpenAI Responses adapter is provider-specific and intentionally excluded
13
13
  // from this unified index. Import it from "@independo/inderun-web/openai".
@@ -1,6 +1,6 @@
1
1
  import type { TaskRequest, TaskResult } from "@independo/inderun-contracts";
2
2
  import type { HostServices } from "../../core/host.js";
3
- import type { ProviderAdapter, ProviderDescriptor, ProviderDynamicCapabilities, RunContext } from "../../core/provider.js";
3
+ import type { ProviderAdapter, ProviderDescriptor, ProviderDynamicCapabilities, ProviderStreamContext, ProviderStreamEvent, RunContext } from "../../core/provider.js";
4
4
  export declare const DEFAULT_OPENAI_RESPONSES_ENDPOINT = "https://api.openai.com/v1/responses";
5
5
  /**
6
6
  * Configuration for the OpenAI Responses provider.
@@ -84,6 +84,49 @@ export declare class OpenAIResponsesProvider implements ProviderAdapter {
84
84
  * @throws IndeRunException mapped to the standard error taxonomy.
85
85
  */
86
86
  run(request: TaskRequest, context: RunContext): Promise<TaskResult>;
87
+ /**
88
+ * Executes a normalized text-to-text task in Mode 2 against the OpenAI
89
+ * Responses API's server-sent event stream.
90
+ *
91
+ * The request is the Mode 1 body plus `"stream": true`, so both modes stay a
92
+ * single code path up to the transport. The response head is classified
93
+ * before the body is read: a non-2xx is drained to text and run through the
94
+ * same `mapHttpError` as Mode 1, because a 429 must surface as RateLimited
95
+ * rather than as a malformed event stream.
96
+ *
97
+ * Event mapping, from the Responses API's typed SSE events to the canonical
98
+ * provider vocabulary:
99
+ *
100
+ * - `response.output_text.delta` becomes a delta;
101
+ * - `response.completed` and `response.incomplete` become the terminal done,
102
+ * with `finalText`, `usage`, and `finishReason` read off the embedded
103
+ * response object by the same helpers Mode 1 uses — it has the same shape;
104
+ * - `response.failed` and `error` become a terminal error;
105
+ * - every other event type is ignored, since the set is open and additive.
106
+ *
107
+ * @param request - Canonical IndeRun task request.
108
+ * @param context - Engine stream context: run id, host services, abort signal.
109
+ */
110
+ stream(request: TaskRequest, context: ProviderStreamContext): AsyncGenerator<ProviderStreamEvent>;
111
+ /**
112
+ * Maps a `response.failed` / `error` stream event onto the error taxonomy.
113
+ * These arrive over an HTTP 200 body, so there is no status code to classify
114
+ * from; OpenAI reports rate limiting and auth failures here with the same
115
+ * `code` values it uses in unary error bodies.
116
+ *
117
+ * Two payload shapes exist and both are accepted: the standalone `error`
118
+ * event carries `code`/`message` at its root, while `response.failed` nests
119
+ * them under `response.error`. The root `type` is the event name rather than
120
+ * an error type, so it is never read as one.
121
+ */
122
+ private mapStreamError;
123
+ /**
124
+ * Resolves the request headers, including the bearer token behind
125
+ * `authContextRef`. Shared by `run` and `stream`: the credential never travels
126
+ * in the request payload, only the slot id does, so both modes must resolve it
127
+ * the same way.
128
+ */
129
+ private resolveHeaders;
87
130
  private createRequestBody;
88
131
  /**
89
132
  * Maps an OpenAI HTTP error response onto the IndeRun error taxonomy:
@@ -1 +1 @@
1
- {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/providers/openai/provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAQ5E,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EAClB,2BAA2B,EAC3B,UAAU,EACX,MAAM,wBAAwB,CAAC;AAEhC,eAAO,MAAM,iCAAiC,wCAAwC,CAAC;AAKvF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,IAAI,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAAC;IACjC;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAwBD;;;;;;GAMG;AACH,qBAAa,uBAAwB,YAAW,eAAe;IAUjD,OAAO,CAAC,QAAQ,CAAC,OAAO;IATpC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA4B;IACjD,OAAO,CAAC,YAAY,CAAC,CAA6D;IAElF;;;OAGG;gBAC0B,OAAO,EAAE,qBAAqB;IAM3D;;OAEG;IACH,QAAQ,IAAI,kBAAkB;IAsB9B;;;;;;;;OAQG;IACG,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,2BAA2B,CAAC;YAkB9D,sBAAsB;IAuCpC;;;;;;OAMG;IACG,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IAkHzE,OAAO,CAAC,iBAAiB;IAuBzB;;;;;;;OAOG;IACH,OAAO,CAAC,YAAY;CAmErB"}
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../../src/providers/openai/provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAQ5E,OAAO,KAAK,EAAE,YAAY,EAAe,MAAM,oBAAoB,CAAC;AACpE,OAAO,KAAK,EACV,eAAe,EACf,kBAAkB,EAClB,2BAA2B,EAC3B,qBAAqB,EACrB,mBAAmB,EACnB,UAAU,EACX,MAAM,wBAAwB,CAAC;AA4BhC,eAAO,MAAM,iCAAiC,wCAAwC,CAAC;AAKvF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;OAEG;IACH,KAAK,EAAE,MAAM,CAAC;IACd;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,IAAI,CAAC,EAAE,gBAAgB,GAAG,MAAM,CAAC;IACjC;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AA6BD;;;;;;GAMG;AACH,qBAAa,uBAAwB,YAAW,eAAe;IAUjD,OAAO,CAAC,QAAQ,CAAC,OAAO;IATpC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA4B;IACjD,OAAO,CAAC,YAAY,CAAC,CAA6D;IAElF;;;OAGG;gBAC0B,OAAO,EAAE,qBAAqB;IAM3D;;OAEG;IACH,QAAQ,IAAI,kBAAkB;IAuB9B;;;;;;;;OAQG;IACG,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,2BAA2B,CAAC;YA+B9D,sBAAsB;IAuCpC;;;;;;OAMG;IACG,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IAiFzE;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACI,MAAM,CACX,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAAC,mBAAmB,CAAC;IAiGtC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,cAAc;IAmBtB;;;;;OAKG;YACW,cAAc;IA2C5B,OAAO,CAAC,iBAAiB;IA2BzB;;;;;;;OAOG;IACH,OAAO,CAAC,YAAY;CAmErB"}