@independo/inderun-web 0.3.0-dev.1 → 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 CHANGED
@@ -30,10 +30,49 @@ const result = await inderun.run({
30
30
  });
31
31
  ```
32
32
 
33
+ ## Streaming
34
+
35
+ `stream()` returns the run handle, its canonical `StreamEvent` sequence, and a `cancel()` hook:
36
+
37
+ ```ts
38
+ const { handle, events, cancel } = await inderun.stream({
39
+ schemaVersion: "1.0",
40
+ task: { kind: "text_to_text" },
41
+ prompt: "Write a one-sentence summary of IndeRun."
42
+ });
43
+
44
+ for await (const event of events) {
45
+ if (event.type === "content_delta") process.stdout.write(event.payload.text);
46
+ if (event.type === "terminal") console.log(event.payload.outcome);
47
+ }
48
+ ```
49
+
50
+ Order by `event.sequence`, not by arrival: it is the ordering authority for a run. Treat an
51
+ unrecognized `event.type` as ignore-or-pass-through — the set is open and additive. Exactly one
52
+ `terminal` event is produced per run, and `cancel()` is idempotent.
53
+
54
+ Streaming needs a host that can deliver a response body incrementally.
55
+ `createBrowserHostServices` provides one; a custom host without a
56
+ `streamingHttpClient` still runs Mode 1, and a stream request is refused at routing
57
+ time with a `streaming_unavailable` reason rather than failing later.
58
+
59
+ ### Custom OpenAI-compatible endpoints
60
+
61
+ The adapter speaks the OpenAI **Responses** API, not chat completions. A custom endpoint must
62
+ accept `"stream": true` and emit `text/event-stream` with the Responses event types —
63
+ `response.output_text.delta`, and `response.completed` / `response.incomplete` carrying the
64
+ response object. Unknown event types are ignored, so an endpoint that emits extra events is
65
+ fine; one that emits a different vocabulary entirely is not.
66
+
33
67
  ## Security Model
34
68
 
35
69
  Browser apps should use a proxy endpoint and keep provider credentials server-side. `createIndeRunWeb` rejects direct calls to the public OpenAI Responses endpoint unless `allowDirectOpenAIEndpoint: true` is set for a controlled environment.
36
70
 
71
+ This applies to streaming too, and is the reason `@independo/inderun-demo-proxy` relays event
72
+ streams rather than buffering them: a browser cannot open its own authenticated connection to
73
+ OpenAI without shipping the key in client code. Never embed a developer-owned API key in
74
+ distributed client code — put it behind a trusted backend proxy and point `endpointUrl` at that.
75
+
37
76
  ## Advanced: registering the OpenAI provider directly
38
77
 
39
78
  `createIndeRunWeb` wires the OpenAI Responses provider for you. To register it
@@ -1,4 +1,4 @@
1
- import type { ClockService, ConnectivityService, HostServices, HttpClientService, HttpRequest, HttpResponse } from "./host.js";
1
+ import type { ClockService, ConnectivityService, HostServices, HttpClientService, HttpRequest, HttpResponse, HttpStreamResponse, HttpStreamingClientService } from "./host.js";
2
2
  /**
3
3
  * Configuration for the fetch-backed HTTP host service.
4
4
  */
@@ -49,12 +49,39 @@ export declare class FetchHttpClient implements HttpClientService {
49
49
  */
50
50
  send(request: HttpRequest): Promise<HttpResponse>;
51
51
  }
52
+ /**
53
+ * Streaming HTTP client service that adapts `fetch` to IndeRun's incremental
54
+ * HTTP contract by handing back `Response.body` rather than buffering it.
55
+ *
56
+ * Two abort sources are merged onto one controller: `timeoutMs`, which bounds
57
+ * only the wait for the response head, and the caller's `signal`, which stays
58
+ * live for the whole body. The timeout timer is therefore cleared as soon as
59
+ * the head resolves — a stream that legitimately stays open for minutes must
60
+ * not be killed by a time-to-first-byte budget.
61
+ */
62
+ export declare class FetchStreamingHttpClient implements HttpStreamingClientService {
63
+ private readonly fetchFn;
64
+ /**
65
+ * Creates a fetch-backed streaming HTTP client.
66
+ * @param options - Optional fetch implementation override.
67
+ * @throws Error when no fetch implementation is available.
68
+ */
69
+ constructor(options?: FetchHttpClientOptions);
70
+ /**
71
+ * Sends a normalized HTTP request and resolves once the response head is available.
72
+ * @param request - Normalized HTTP request payload.
73
+ * @param signal - Optional caller-driven cancellation covering the whole response.
74
+ * @returns Response head plus a lazily-consumed body chunk sequence.
75
+ */
76
+ stream(request: HttpRequest, signal?: AbortSignal): Promise<HttpStreamResponse>;
77
+ }
52
78
  /**
53
79
  * Options for constructing default browser host services.
54
80
  */
55
81
  export interface CreateBrowserHostServicesOptions extends Partial<HostServices> {
56
82
  /**
57
- * Optional fetch implementation used when `httpClient` is not supplied.
83
+ * Optional fetch implementation used when `httpClient`/`streamingHttpClient`
84
+ * are not supplied.
58
85
  */
59
86
  fetchFn?: typeof fetch;
60
87
  }
@@ -1 +1 @@
1
- {"version":3,"file":"browser-host.d.ts","sourceRoot":"","sources":["../../src/core/browser-host.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,YAAY,EACb,MAAM,WAAW,CAAC;AAEnB;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAED;;GAEG;AACH,qBAAa,0BAA2B,YAAW,mBAAmB;IACpE;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;CAOnC;AAED;;GAEG;AACH,qBAAa,kBAAmB,YAAW,YAAY;IACrD;;OAEG;IACH,GAAG,IAAI,MAAM;IAIb;;OAEG;IACH,YAAY,IAAI,MAAM;CAOvB;AAED;;GAEG;AACH,qBAAa,eAAgB,YAAW,iBAAiB;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IAEvC;;;;OAIG;gBACS,OAAO,GAAE,sBAA2B;IAShD;;;;OAIG;IACG,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;CA0CxD;AAED;;GAEG;AACH,MAAM,WAAW,gCAAiC,SAAQ,OAAO,CAAC,YAAY,CAAC;IAC7E;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,GAAE,gCAAqC,GAC7C,YAAY,CA2Bd"}
1
+ {"version":3,"file":"browser-host.d.ts","sourceRoot":"","sources":["../../src/core/browser-host.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,mBAAmB,EACnB,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,YAAY,EACZ,kBAAkB,EAClB,0BAA0B,EAC3B,MAAM,WAAW,CAAC;AAEnB;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;OAEG;IACH,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAED;;GAEG;AACH,qBAAa,0BAA2B,YAAW,mBAAmB;IACpE;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAC;CAOnC;AAED;;GAEG;AACH,qBAAa,kBAAmB,YAAW,YAAY;IACrD;;OAEG;IACH,GAAG,IAAI,MAAM;IAIb;;OAEG;IACH,YAAY,IAAI,MAAM;CAOvB;AAED;;GAEG;AACH,qBAAa,eAAgB,YAAW,iBAAiB;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IAEvC;;;;OAIG;gBACS,OAAO,GAAE,sBAA2B;IAShD;;;;OAIG;IACG,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC;CA0CxD;AAED;;;;;;;;;GASG;AACH,qBAAa,wBAAyB,YAAW,0BAA0B;IACzE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IAEvC;;;;OAIG;gBACS,OAAO,GAAE,sBAA2B;IAShD;;;;;OAKG;IACG,MAAM,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAwHtF;AAED;;GAEG;AACH,MAAM,WAAW,gCAAiC,SAAQ,OAAO,CAAC,YAAY,CAAC;IAC7E;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,GAAE,gCAAqC,GAC7C,YAAY,CAmCd"}
@@ -93,6 +93,154 @@ export class FetchHttpClient {
93
93
  }
94
94
  }
95
95
  }
96
+ /**
97
+ * Streaming HTTP client service that adapts `fetch` to IndeRun's incremental
98
+ * HTTP contract by handing back `Response.body` rather than buffering it.
99
+ *
100
+ * Two abort sources are merged onto one controller: `timeoutMs`, which bounds
101
+ * only the wait for the response head, and the caller's `signal`, which stays
102
+ * live for the whole body. The timeout timer is therefore cleared as soon as
103
+ * the head resolves — a stream that legitimately stays open for minutes must
104
+ * not be killed by a time-to-first-byte budget.
105
+ */
106
+ export class FetchStreamingHttpClient {
107
+ fetchFn;
108
+ /**
109
+ * Creates a fetch-backed streaming HTTP client.
110
+ * @param options - Optional fetch implementation override.
111
+ * @throws Error when no fetch implementation is available.
112
+ */
113
+ constructor(options = {}) {
114
+ const fetchFn = options.fetchFn ?? globalThis.fetch?.bind(globalThis);
115
+ if (!fetchFn) {
116
+ throw new Error("FetchStreamingHttpClient requires a fetch implementation.");
117
+ }
118
+ this.fetchFn = fetchFn;
119
+ }
120
+ /**
121
+ * Sends a normalized HTTP request and resolves once the response head is available.
122
+ * @param request - Normalized HTTP request payload.
123
+ * @param signal - Optional caller-driven cancellation covering the whole response.
124
+ * @returns Response head plus a lazily-consumed body chunk sequence.
125
+ */
126
+ async stream(request, signal) {
127
+ const controller = typeof AbortController === "undefined" ? undefined : new AbortController();
128
+ let timeoutId;
129
+ let onAbort;
130
+ if (controller) {
131
+ if (request.timeoutMs !== undefined) {
132
+ timeoutId = setTimeout(() => controller.abort(), request.timeoutMs);
133
+ }
134
+ if (signal) {
135
+ if (signal.aborted) {
136
+ controller.abort();
137
+ }
138
+ else {
139
+ onAbort = () => controller.abort();
140
+ signal.addEventListener("abort", onAbort, { once: true });
141
+ }
142
+ }
143
+ }
144
+ const clearTimeoutTimer = () => {
145
+ if (timeoutId !== undefined) {
146
+ clearTimeout(timeoutId);
147
+ timeoutId = undefined;
148
+ }
149
+ };
150
+ const releaseAbortListener = () => {
151
+ if (signal && onAbort) {
152
+ signal.removeEventListener("abort", onAbort);
153
+ onAbort = undefined;
154
+ }
155
+ };
156
+ let response;
157
+ try {
158
+ const init = { method: request.method };
159
+ if (request.headers) {
160
+ init.headers = request.headers;
161
+ }
162
+ if (request.body !== undefined) {
163
+ init.body = request.body;
164
+ }
165
+ if (controller) {
166
+ init.signal = controller.signal;
167
+ }
168
+ response = await this.fetchFn(request.url, init);
169
+ }
170
+ catch (error) {
171
+ clearTimeoutTimer();
172
+ releaseAbortListener();
173
+ throw error;
174
+ }
175
+ clearTimeoutTimer();
176
+ const headers = {};
177
+ response.headers.forEach((value, key) => {
178
+ headers[key.toLowerCase()] = value;
179
+ });
180
+ const stream = response.body;
181
+ async function* iterate() {
182
+ try {
183
+ if (!stream) {
184
+ // A bodyless response (204, HEAD) or a fetch polyfill without stream
185
+ // support: fall back to the buffered text so callers still terminate.
186
+ const text = await response.text();
187
+ if (text.length > 0) {
188
+ yield new TextEncoder().encode(text);
189
+ }
190
+ return;
191
+ }
192
+ const reader = stream.getReader();
193
+ // The body is also raced against `signal` directly, not just against the
194
+ // fetch-level abort: a fetch implementation is free not to error an
195
+ // in-flight body when its request signal fires, and the caller's
196
+ // cancellation guarantee must not depend on that.
197
+ let onBodyAbort;
198
+ const aborted = new Promise((_resolve, reject) => {
199
+ if (!signal)
200
+ return;
201
+ onBodyAbort = () => reject(Object.assign(new Error("The stream was aborted."), { name: "AbortError" }));
202
+ if (signal.aborted) {
203
+ onBodyAbort();
204
+ }
205
+ else {
206
+ signal.addEventListener("abort", onBodyAbort, { once: true });
207
+ }
208
+ });
209
+ // Keeps an abort that arrives after the body finished from surfacing as
210
+ // an unhandled rejection; the race below still observes it.
211
+ void aborted.catch(() => undefined);
212
+ try {
213
+ for (;;) {
214
+ const { done, value } = await Promise.race([reader.read(), aborted]);
215
+ if (done)
216
+ return;
217
+ if (value)
218
+ yield value;
219
+ }
220
+ }
221
+ catch (error) {
222
+ await reader.cancel().catch(() => undefined);
223
+ throw error;
224
+ }
225
+ finally {
226
+ if (signal && onBodyAbort) {
227
+ signal.removeEventListener("abort", onBodyAbort);
228
+ }
229
+ reader.releaseLock();
230
+ }
231
+ }
232
+ finally {
233
+ releaseAbortListener();
234
+ }
235
+ }
236
+ return {
237
+ status: response.status,
238
+ statusText: response.statusText,
239
+ headers,
240
+ body: iterate()
241
+ };
242
+ }
243
+ }
96
244
  /**
97
245
  * Creates a browser-oriented HostServices bundle for the Web SDK.
98
246
  * Supplied host services override the defaults; missing connectivity, clock, and HTTP services are created.
@@ -112,6 +260,15 @@ export function createBrowserHostServices(options = {}) {
112
260
  else {
113
261
  hostServices.httpClient = new FetchHttpClient();
114
262
  }
263
+ if (options.streamingHttpClient) {
264
+ hostServices.streamingHttpClient = options.streamingHttpClient;
265
+ }
266
+ else if (options.fetchFn) {
267
+ hostServices.streamingHttpClient = new FetchStreamingHttpClient({ fetchFn: options.fetchFn });
268
+ }
269
+ else {
270
+ hostServices.streamingHttpClient = new FetchStreamingHttpClient();
271
+ }
115
272
  if (options.deviceConstraints) {
116
273
  hostServices.deviceConstraints = options.deviceConstraints;
117
274
  }
@@ -1,9 +1,15 @@
1
1
  import { type TaskRequest, type TaskResult } from "@independo/inderun-contracts";
2
2
  import type { HostServices } from "./host.js";
3
- import type { ProviderCapabilitySnapshot } from "./provider.js";
3
+ import type { ProviderCapabilitySnapshot, StreamRun } from "./provider.js";
4
4
  import type { IndeRunApi } from "./generated/inderun-api.js";
5
5
  import { ProviderRegistry } from "./registry.js";
6
6
  import { type TelemetryService } from "./telemetry.js";
7
+ /**
8
+ * @deprecated Renamed to `StreamRun`, which is the name the generated
9
+ * `IndeRunApi` surface and the Swift/Kotlin SDKs use. Kept as an alias so the
10
+ * rename is not a breaking change.
11
+ */
12
+ export type StreamHandleResult = StreamRun;
7
13
  /**
8
14
  * Main orchestrator SDK entrypoint class.
9
15
  * Validates tasks, executes routing rules based on request constraints and connectivity,
@@ -21,6 +27,17 @@ export declare class IndeRun implements IndeRunApi {
21
27
  * @param telemetryService - Optional custom telemetry service listener.
22
28
  */
23
29
  constructor(registry: ProviderRegistry, hostServices: HostServices, telemetryService?: TelemetryService);
30
+ /**
31
+ * Current time from the host clock, falling back to wall-clock time when the
32
+ * host does not supply one.
33
+ */
34
+ private now;
35
+ /**
36
+ * Emits `route_decided` for a completed route selection. Shared by `run()` and
37
+ * `stream()`: the payload is identical for both modes, and the interaction
38
+ * mode is already reflected in the plan the router returned.
39
+ */
40
+ private emitRouteDecided;
24
41
  /**
25
42
  * Safely emits a telemetry event by swallowing any internal logging/telemetry system errors.
26
43
  * @param event - The telemetry event to emit.
@@ -40,6 +57,25 @@ export declare class IndeRun implements IndeRunApi {
40
57
  * @throws {IndeRunException} Standardized error indicating validation, connection, or provider failures.
41
58
  */
42
59
  run(request: TaskRequest): Promise<TaskResult>;
60
+ /**
61
+ * Orchestrates a Mode 2 streaming execution of a TaskRequest.
62
+ *
63
+ * Routes through the same planner as run(), asking it for a `stream` route: the
64
+ * planner rejects providers that do not declare `supports.streaming` or whose
65
+ * dynamic capability snapshot says they cannot stream here, and reports why. The
66
+ * whole returned chain is therefore mode-compatible and subject to the same
67
+ * privacy, locality, availability, and policy constraints as a Mode 1 route.
68
+ *
69
+ * Fallback to the next provider is allowed only before the first content event
70
+ * has been admitted for the run; once one has been delivered, the run is
71
+ * committed to that provider and any later provider failure becomes a terminal
72
+ * error outcome rather than a silent provider swap — partial output spliced from
73
+ * two providers is undetectable to the caller. Cancellation always forecloses
74
+ * fallback, regardless of commit state, and is idempotent.
75
+ * @param request - The canonical task request containing prompts, tasks, and constraints.
76
+ * @throws {IndeRunException} If validation fails or no eligible streaming provider exists.
77
+ */
78
+ stream(request: TaskRequest): Promise<StreamRun>;
43
79
  /**
44
80
  * Reports each registered provider's static descriptor and current dynamic capability check,
45
81
  * without executing a task. Useful for UI that shows live provider availability before a run.
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/core/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,WAAW,EAChB,KAAK,UAAU,EAChB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAGjD,OAAO,EAAuB,KAAK,gBAAgB,EAAwB,MAAM,gBAAgB,CAAC;AAElG;;;;GAIG;AACH,qBAAa,OAAQ,YAAW,UAAU;IAWtC,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,YAAY;IAXtB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,gBAAgB,CAAmB;IAE3C;;;;;OAKG;gBAEO,QAAQ,EAAE,gBAAgB,EAC1B,YAAY,EAAE,YAAY,EAClC,gBAAgB,CAAC,EAAE,gBAAgB;IAOrC;;;OAGG;IACH,OAAO,CAAC,QAAQ;IAShB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAoBxB;;;;;;;OAOG;IACG,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAuIpD;;;OAGG;IACG,iBAAiB,IAAI,OAAO,CAAC,0BAA0B,EAAE,CAAC;CASjE"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/core/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,WAAW,EAChB,KAAK,UAAU,EAChB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAmB,0BAA0B,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC5F,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAQjD,OAAO,EAAuB,KAAK,gBAAgB,EAAwB,MAAM,gBAAgB,CAAC;AAGlG;;;;GAIG;AACH,MAAM,MAAM,kBAAkB,GAAG,SAAS,CAAC;AAE3C;;;;GAIG;AACH,qBAAa,OAAQ,YAAW,UAAU;IAWtC,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,YAAY;IAXtB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,gBAAgB,CAAmB;IAE3C;;;;;OAKG;gBAEO,QAAQ,EAAE,gBAAgB,EAC1B,YAAY,EAAE,YAAY,EAClC,gBAAgB,CAAC,EAAE,gBAAgB;IAOrC;;;OAGG;IACH,OAAO,CAAC,GAAG;IAIX;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAuBxB;;;OAGG;IACH,OAAO,CAAC,QAAQ;IAShB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAoBxB;;;;;;;OAOG;IACG,GAAG,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;IAsHpD;;;;;;;;;;;;;;;;;OAiBG;IACG,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,SAAS,CAAC;IA+PtD;;;OAGG;IACG,iBAAiB,IAAI,OAAO,CAAC,0BAA0B,EAAE,CAAC;CASjE"}