@independo/inderun-web 0.3.0-dev.12 → 0.3.0-dev.14
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 +2 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +47 -2
- package/dist/onnx.d.ts +21 -0
- package/dist/onnx.d.ts.map +1 -1
- package/dist/onnx.js +15 -0
- package/dist/openai.d.ts +7 -0
- package/dist/openai.d.ts.map +1 -1
- package/dist/openai.js +7 -0
- package/dist/system-model.d.ts +13 -0
- package/dist/system-model.d.ts.map +1 -1
- package/dist/system-model.js +13 -0
- package/package.json +10 -6
- package/src/core/browser-host.ts +339 -0
- package/src/core/engine.ts +540 -0
- package/src/core/errors.ts +220 -0
- package/src/core/event-gate.ts +71 -0
- package/src/core/generated/inderun-api.ts +19 -0
- package/src/core/host.ts +15 -0
- package/src/core/provider.ts +231 -0
- package/src/core/registry.ts +48 -0
- package/src/core/route-planner.ts +261 -0
- package/src/core/router.ts +149 -0
- package/src/core/sse.ts +134 -0
- package/src/core/telemetry.ts +20 -0
- package/src/index.ts +147 -0
- package/src/onnx.ts +57 -0
- package/src/openai.ts +21 -0
- package/src/providers/onnx/provider.ts +300 -0
- package/src/providers/onnx/runtime.ts +188 -0
- package/src/providers/onnx/transformers-runtime.ts +345 -0
- package/src/providers/openai/provider.ts +765 -0
- package/src/providers/system-model/chrome-runtime.ts +221 -0
- package/src/providers/system-model/provider.ts +260 -0
- package/src/providers/system-model/runtime.ts +202 -0
- package/src/system-model.ts +49 -0
- package/src/web-sdk.ts +114 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getTaskRequestValidationIssues,
|
|
3
|
+
type StreamEvent,
|
|
4
|
+
type StreamRunHandle,
|
|
5
|
+
type StreamTerminalOutcome,
|
|
6
|
+
type TaskRequest,
|
|
7
|
+
type TaskResult
|
|
8
|
+
} from "@independo/inderun-contracts";
|
|
9
|
+
import type { HostServices } from "./host.js";
|
|
10
|
+
import type { ProviderAdapter, ProviderCapabilitySnapshot, StreamRun } from "./provider.js";
|
|
11
|
+
import type { IndeRunApi } from "./generated/inderun-api.js";
|
|
12
|
+
import { ProviderRegistry } from "./registry.js";
|
|
13
|
+
import { type RouteSelection, Router } from "./router.js";
|
|
14
|
+
import {
|
|
15
|
+
createCapabilityMismatch,
|
|
16
|
+
createInternal,
|
|
17
|
+
createUnavailable,
|
|
18
|
+
toIndeRunException
|
|
19
|
+
} from "./errors.js";
|
|
20
|
+
import { type TelemetryEvent, type TelemetryService, NoOpTelemetryService } from "./telemetry.js";
|
|
21
|
+
import { EventGate } from "./event-gate.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @deprecated Renamed to `StreamRun`, which is the name the generated
|
|
25
|
+
* `IndeRunApi` surface and the Swift/Kotlin SDKs use. Kept as an alias so the
|
|
26
|
+
* rename is not a breaking change.
|
|
27
|
+
*/
|
|
28
|
+
export type StreamHandleResult = StreamRun;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Main orchestrator SDK entrypoint class.
|
|
32
|
+
* Validates tasks, executes routing rules based on request constraints and connectivity,
|
|
33
|
+
* triggers provider adapters, and logs timing telemetry.
|
|
34
|
+
*/
|
|
35
|
+
export class IndeRun implements IndeRunApi {
|
|
36
|
+
private router: Router;
|
|
37
|
+
private telemetryService: TelemetryService;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Initializes the IndeRun instance.
|
|
41
|
+
* @param registry - Registry filled with active providers.
|
|
42
|
+
* @param hostServices - Services wrapping OS interfaces and indicators.
|
|
43
|
+
* @param telemetryService - Optional custom telemetry service listener.
|
|
44
|
+
*/
|
|
45
|
+
constructor(
|
|
46
|
+
private registry: ProviderRegistry,
|
|
47
|
+
private hostServices: HostServices,
|
|
48
|
+
telemetryService?: TelemetryService
|
|
49
|
+
) {
|
|
50
|
+
this.router = new Router(this.registry);
|
|
51
|
+
this.telemetryService =
|
|
52
|
+
telemetryService || this.hostServices.telemetry || new NoOpTelemetryService();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Current time from the host clock, falling back to wall-clock time when the
|
|
57
|
+
* host does not supply one.
|
|
58
|
+
*/
|
|
59
|
+
private now(): number {
|
|
60
|
+
return this.hostServices.clock ? this.hostServices.clock.now() : Date.now();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Emits `route_decided` for a completed route selection. Shared by `run()` and
|
|
65
|
+
* `stream()`: the payload is identical for both modes, and the interaction
|
|
66
|
+
* mode is already reflected in the plan the router returned.
|
|
67
|
+
*/
|
|
68
|
+
private emitRouteDecided(
|
|
69
|
+
runId: string,
|
|
70
|
+
request: TaskRequest,
|
|
71
|
+
routeSelection: RouteSelection,
|
|
72
|
+
providerCount: number
|
|
73
|
+
): void {
|
|
74
|
+
this.safeEmit({
|
|
75
|
+
type: "route_decided",
|
|
76
|
+
runId,
|
|
77
|
+
timestamp: this.now(),
|
|
78
|
+
payload: {
|
|
79
|
+
selectedProviderId: routeSelection.routePlan.selectedProviderId,
|
|
80
|
+
fallbackProviderIds: routeSelection.routePlan.fallbackProviderIds,
|
|
81
|
+
rejectedProviders: routeSelection.routePlan.rejectedProviders,
|
|
82
|
+
fallbackAvailable: providerCount > 1,
|
|
83
|
+
taskKind: request.task.kind,
|
|
84
|
+
constraints: request.constraints ?? null,
|
|
85
|
+
preferences: request.preferences ?? null,
|
|
86
|
+
explanation: routeSelection.explanation
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Safely emits a telemetry event by swallowing any internal logging/telemetry system errors.
|
|
93
|
+
* @param event - The telemetry event to emit.
|
|
94
|
+
*/
|
|
95
|
+
private safeEmit(event: TelemetryEvent): void {
|
|
96
|
+
try {
|
|
97
|
+
this.telemetryService.emit(event);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
// Telemetry failures must never disrupt primary execution flows.
|
|
100
|
+
console.warn("[IndeRun] Telemetry service emission failed:", err);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Returns a stable, generic description of an error class for privacy-preserving telemetry.
|
|
106
|
+
* @param errorClass - The taxonomy classification of the exception.
|
|
107
|
+
*/
|
|
108
|
+
private getStableMessage(errorClass: string): string {
|
|
109
|
+
switch (errorClass) {
|
|
110
|
+
case "CapabilityMismatch":
|
|
111
|
+
return "Provider capability mismatch.";
|
|
112
|
+
case "Offline":
|
|
113
|
+
return "Device is offline.";
|
|
114
|
+
case "AuthError":
|
|
115
|
+
return "Authentication failed.";
|
|
116
|
+
case "RateLimited":
|
|
117
|
+
return "Rate limit exceeded.";
|
|
118
|
+
case "Timeout":
|
|
119
|
+
return "Execution timed out.";
|
|
120
|
+
case "Unavailable":
|
|
121
|
+
return "Provider is unavailable.";
|
|
122
|
+
case "Internal":
|
|
123
|
+
default:
|
|
124
|
+
return "An internal engine error occurred.";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Orchestrates the execution of a TaskRequest.
|
|
130
|
+
* Performs JSON Schema validation, selects a provider via routing rules,
|
|
131
|
+
* measures total execution time, and attaches telemetry metadata to the outcome.
|
|
132
|
+
* @param request - The canonical task request containing prompts, tasks, and constraints.
|
|
133
|
+
* @returns Canonical task output containing output details and telemetry metadata.
|
|
134
|
+
* @throws {IndeRunException} Standardized error indicating validation, connection, or provider failures.
|
|
135
|
+
*/
|
|
136
|
+
async run(request: TaskRequest): Promise<TaskResult> {
|
|
137
|
+
const startTime = this.now();
|
|
138
|
+
|
|
139
|
+
const runId = request.requestId || `run_${Math.random().toString(36).substring(2, 11)}`;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
// 1. Validate request payload using schema contracts
|
|
143
|
+
const validationIssues = getTaskRequestValidationIssues(request);
|
|
144
|
+
if (validationIssues.length > 0) {
|
|
145
|
+
const message = `Validation failed for TaskRequest: ${validationIssues
|
|
146
|
+
.map((i) => `${i.path} - ${i.message}`)
|
|
147
|
+
.join("; ")}`;
|
|
148
|
+
throw createInternal(message, {
|
|
149
|
+
runId,
|
|
150
|
+
details: { validationIssues }
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 2. Select the route based on constraints and host capabilities
|
|
155
|
+
const routeSelection = await this.router.selectRoute(request, this.hostServices, "run");
|
|
156
|
+
const providers = [routeSelection.provider, ...routeSelection.fallbackProviders];
|
|
157
|
+
|
|
158
|
+
this.emitRouteDecided(runId, request, routeSelection, providers.length);
|
|
159
|
+
|
|
160
|
+
// 3. Execute the run task using the planned provider chain
|
|
161
|
+
const attemptedProviderIds: string[] = [];
|
|
162
|
+
let lastError: unknown;
|
|
163
|
+
for (const [index, provider] of providers.entries()) {
|
|
164
|
+
const providerId = provider.describe().id;
|
|
165
|
+
attemptedProviderIds.push(providerId);
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const result = await provider.run(request, {
|
|
169
|
+
runId,
|
|
170
|
+
hostServices: this.hostServices
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const endTime = this.now();
|
|
174
|
+
const totalMs = endTime - startTime;
|
|
175
|
+
|
|
176
|
+
result.runId = runId;
|
|
177
|
+
result.telemetry = {
|
|
178
|
+
...result.telemetry,
|
|
179
|
+
providerUsed: providerId,
|
|
180
|
+
totalMs
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
this.safeEmit({
|
|
184
|
+
type: "attempt_succeeded",
|
|
185
|
+
runId,
|
|
186
|
+
timestamp: endTime,
|
|
187
|
+
payload: {
|
|
188
|
+
providerId,
|
|
189
|
+
durationMs: totalMs,
|
|
190
|
+
fallbackOccurred: index > 0,
|
|
191
|
+
attemptedProviderIds
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
return result;
|
|
196
|
+
} catch (err) {
|
|
197
|
+
lastError = toIndeRunException(err, {
|
|
198
|
+
providerId,
|
|
199
|
+
runId,
|
|
200
|
+
details: {
|
|
201
|
+
attemptedProviderIds,
|
|
202
|
+
fallbackOccurred: index > 0,
|
|
203
|
+
routePlan: routeSelection.routePlan
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const endTime = this.now();
|
|
210
|
+
const totalMs = endTime - startTime;
|
|
211
|
+
const exception = toIndeRunException(lastError ?? new Error("No providers were attempted."), {
|
|
212
|
+
runId,
|
|
213
|
+
details: {
|
|
214
|
+
totalMs,
|
|
215
|
+
attemptedProviderIds,
|
|
216
|
+
fallbackOccurred: providers.length > 1,
|
|
217
|
+
routePlan: routeSelection.routePlan
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
throw exception;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
const exception = toIndeRunException(error, {
|
|
224
|
+
runId,
|
|
225
|
+
details: {
|
|
226
|
+
totalMs:
|
|
227
|
+
typeof (error as { details?: { totalMs?: number } }).details?.totalMs === "number"
|
|
228
|
+
? (error as { details?: { totalMs?: number } }).details?.totalMs
|
|
229
|
+
: this.now() - startTime
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
const endTime = this.now();
|
|
233
|
+
const totalMs =
|
|
234
|
+
typeof (exception as { details?: { totalMs?: number } }).details?.totalMs === "number"
|
|
235
|
+
? (exception as { details?: { totalMs?: number } }).details?.totalMs
|
|
236
|
+
: endTime - startTime;
|
|
237
|
+
|
|
238
|
+
this.safeEmit({
|
|
239
|
+
type: "attempt_failed",
|
|
240
|
+
runId,
|
|
241
|
+
timestamp: endTime,
|
|
242
|
+
payload: {
|
|
243
|
+
providerId: exception.providerId ?? null,
|
|
244
|
+
durationMs: totalMs,
|
|
245
|
+
errorClass: exception.errorClass,
|
|
246
|
+
message: this.getStableMessage(exception.errorClass)
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
throw exception;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Orchestrates a Mode 2 streaming execution of a TaskRequest.
|
|
256
|
+
*
|
|
257
|
+
* Routes through the same planner as run(), asking it for a `stream` route: the
|
|
258
|
+
* planner rejects providers that do not declare `supports.streaming` or whose
|
|
259
|
+
* dynamic capability snapshot says they cannot stream here, and reports why. The
|
|
260
|
+
* whole returned chain is therefore mode-compatible and subject to the same
|
|
261
|
+
* privacy, locality, availability, and policy constraints as a Mode 1 route.
|
|
262
|
+
*
|
|
263
|
+
* Fallback to the next provider is allowed only before the first content event
|
|
264
|
+
* has been admitted for the run; once one has been delivered, the run is
|
|
265
|
+
* committed to that provider and any later provider failure becomes a terminal
|
|
266
|
+
* error outcome rather than a silent provider swap — partial output spliced from
|
|
267
|
+
* two providers is undetectable to the caller. Cancellation always forecloses
|
|
268
|
+
* fallback, regardless of commit state, and is idempotent.
|
|
269
|
+
* @param request - The canonical task request containing prompts, tasks, and constraints.
|
|
270
|
+
* @throws {IndeRunException} If validation fails or no eligible streaming provider exists.
|
|
271
|
+
*/
|
|
272
|
+
async stream(request: TaskRequest): Promise<StreamRun> {
|
|
273
|
+
const startTime = this.now();
|
|
274
|
+
const now = () => this.now();
|
|
275
|
+
const runId = request.requestId || `run_${Math.random().toString(36).substring(2, 11)}`;
|
|
276
|
+
|
|
277
|
+
const validationIssues = getTaskRequestValidationIssues(request);
|
|
278
|
+
if (validationIssues.length > 0) {
|
|
279
|
+
const message = `Validation failed for TaskRequest: ${validationIssues
|
|
280
|
+
.map((i) => `${i.path} - ${i.message}`)
|
|
281
|
+
.join("; ")}`;
|
|
282
|
+
throw createInternal(message, { runId, details: { validationIssues } });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Route selection failures are the one class of stream error that surfaces as a
|
|
286
|
+
// rejected promise instead of a terminal event, so they must still carry the
|
|
287
|
+
// runId — there is no handle for the caller to correlate them with otherwise.
|
|
288
|
+
let routeSelection: RouteSelection;
|
|
289
|
+
try {
|
|
290
|
+
routeSelection = await this.router.selectRoute(request, this.hostServices, "stream");
|
|
291
|
+
} catch (error) {
|
|
292
|
+
throw toIndeRunException(error, { runId });
|
|
293
|
+
}
|
|
294
|
+
const providers = [routeSelection.provider, ...routeSelection.fallbackProviders];
|
|
295
|
+
|
|
296
|
+
this.emitRouteDecided(runId, request, routeSelection, providers.length);
|
|
297
|
+
|
|
298
|
+
// Defensive only: streaming eligibility is decided by the route planner, which
|
|
299
|
+
// throws with its own rejection reasons when nothing can stream. Reaching here
|
|
300
|
+
// means a planned provider was unregistered between planning and selection.
|
|
301
|
+
if (providers.length === 0) {
|
|
302
|
+
throw createCapabilityMismatch("No eligible provider supports streaming for this request.", {
|
|
303
|
+
runId,
|
|
304
|
+
details: { taskKind: request.task.kind }
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const controller = new AbortController();
|
|
309
|
+
const gate = new EventGate(runId);
|
|
310
|
+
const hostServices = this.hostServices;
|
|
311
|
+
const safeEmit = this.safeEmit.bind(this);
|
|
312
|
+
const getStableMessage = this.getStableMessage.bind(this);
|
|
313
|
+
|
|
314
|
+
const handle: StreamRunHandle = {
|
|
315
|
+
runId,
|
|
316
|
+
schemaVersion: "1.0",
|
|
317
|
+
startedAt: startTime,
|
|
318
|
+
providerId: providers[0]!.describe().id
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
async function* run(): AsyncGenerator<StreamEvent> {
|
|
322
|
+
const attemptedProviderIds: string[] = [];
|
|
323
|
+
let committed = false;
|
|
324
|
+
|
|
325
|
+
const cancelledOutcome = (partialText: string): StreamTerminalOutcome => {
|
|
326
|
+
const reason =
|
|
327
|
+
typeof controller.signal.reason === "string" ? controller.signal.reason : undefined;
|
|
328
|
+
return {
|
|
329
|
+
outcome: "cancelled",
|
|
330
|
+
runId,
|
|
331
|
+
schemaVersion: "1.0",
|
|
332
|
+
partialText,
|
|
333
|
+
...(reason !== undefined ? { reason } : {})
|
|
334
|
+
};
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
for (const [index, provider] of providers.entries()) {
|
|
338
|
+
if (controller.signal.aborted) {
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const providerId = provider.describe().id;
|
|
343
|
+
attemptedProviderIds.push(providerId);
|
|
344
|
+
safeEmit({
|
|
345
|
+
type: "stream_attempt_started",
|
|
346
|
+
runId,
|
|
347
|
+
timestamp: now(),
|
|
348
|
+
payload: { providerId, fallbackOccurred: index > 0 }
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
let partialText = "";
|
|
352
|
+
try {
|
|
353
|
+
const iterable = (provider as Required<Pick<ProviderAdapter, "stream">>).stream(request, {
|
|
354
|
+
runId,
|
|
355
|
+
hostServices: hostServices,
|
|
356
|
+
signal: controller.signal
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
let terminatedInLoop = false;
|
|
360
|
+
for await (const ev of iterable) {
|
|
361
|
+
if (controller.signal.aborted) {
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (ev.kind === "error") {
|
|
366
|
+
throw ev.error;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (ev.kind === "delta" || ev.kind === "snapshot") {
|
|
370
|
+
partialText = ev.kind === "delta" ? partialText + ev.text : ev.text;
|
|
371
|
+
if (!committed) {
|
|
372
|
+
committed = true;
|
|
373
|
+
safeEmit({
|
|
374
|
+
type: "stream_attempt_succeeded",
|
|
375
|
+
runId,
|
|
376
|
+
timestamp: now(),
|
|
377
|
+
payload: { providerId, attemptedProviderIds }
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
const streamEvent = gate.admit({
|
|
381
|
+
timestamp: now(),
|
|
382
|
+
type: ev.kind === "delta" ? "content_delta" : "content_snapshot",
|
|
383
|
+
payload: { text: ev.text }
|
|
384
|
+
});
|
|
385
|
+
if (streamEvent) yield streamEvent;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ev.kind === "done"
|
|
390
|
+
const outcome: StreamTerminalOutcome = {
|
|
391
|
+
outcome: "completed",
|
|
392
|
+
runId,
|
|
393
|
+
schemaVersion: "1.0",
|
|
394
|
+
finalText: ev.finalText,
|
|
395
|
+
telemetry: { providerUsed: providerId, totalMs: now() - startTime },
|
|
396
|
+
...(ev.finishReason !== undefined ? { finishReason: ev.finishReason } : {}),
|
|
397
|
+
...(ev.usage !== undefined ? { usage: ev.usage } : {})
|
|
398
|
+
};
|
|
399
|
+
const terminal = gate.terminate(outcome, now());
|
|
400
|
+
safeEmit({
|
|
401
|
+
type: "stream_completed",
|
|
402
|
+
runId,
|
|
403
|
+
timestamp: now(),
|
|
404
|
+
payload: { providerId, durationMs: now() - startTime, attemptedProviderIds }
|
|
405
|
+
});
|
|
406
|
+
terminatedInLoop = true;
|
|
407
|
+
if (terminal) yield terminal;
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (terminatedInLoop) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (controller.signal.aborted) {
|
|
416
|
+
const terminal = gate.terminate(cancelledOutcome(partialText), now());
|
|
417
|
+
safeEmit({
|
|
418
|
+
type: "stream_cancelled",
|
|
419
|
+
runId,
|
|
420
|
+
timestamp: now(),
|
|
421
|
+
payload: { providerId, attemptedProviderIds }
|
|
422
|
+
});
|
|
423
|
+
if (terminal) yield terminal;
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Provider's iterable ended without a terminal "done"/"error" event.
|
|
428
|
+
throw new Error(`Provider '${providerId}' stream ended without a terminal event.`);
|
|
429
|
+
} catch (err) {
|
|
430
|
+
if (controller.signal.aborted) {
|
|
431
|
+
const terminal = gate.terminate(cancelledOutcome(partialText), now());
|
|
432
|
+
safeEmit({
|
|
433
|
+
type: "stream_cancelled",
|
|
434
|
+
runId,
|
|
435
|
+
timestamp: now(),
|
|
436
|
+
payload: { providerId, attemptedProviderIds }
|
|
437
|
+
});
|
|
438
|
+
if (terminal) yield terminal;
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const exception = toIndeRunException(err, {
|
|
443
|
+
providerId,
|
|
444
|
+
runId,
|
|
445
|
+
details: { attemptedProviderIds }
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
if (!committed) {
|
|
449
|
+
safeEmit({
|
|
450
|
+
type: "stream_attempt_failed",
|
|
451
|
+
runId,
|
|
452
|
+
timestamp: now(),
|
|
453
|
+
payload: {
|
|
454
|
+
providerId,
|
|
455
|
+
errorClass: exception.errorClass,
|
|
456
|
+
message: getStableMessage(exception.errorClass)
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const outcome: StreamTerminalOutcome = {
|
|
463
|
+
outcome: "error",
|
|
464
|
+
runId,
|
|
465
|
+
schemaVersion: "1.0",
|
|
466
|
+
error: exception.toContractError(),
|
|
467
|
+
partialText
|
|
468
|
+
};
|
|
469
|
+
const terminal = gate.terminate(outcome, now());
|
|
470
|
+
safeEmit({
|
|
471
|
+
type: "stream_failed",
|
|
472
|
+
runId,
|
|
473
|
+
timestamp: now(),
|
|
474
|
+
payload: {
|
|
475
|
+
providerId,
|
|
476
|
+
errorClass: exception.errorClass,
|
|
477
|
+
message: getStableMessage(exception.errorClass),
|
|
478
|
+
attemptedProviderIds
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
if (terminal) yield terminal;
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Every provider failed pre-commit, or cancellation landed before any attempt started.
|
|
487
|
+
if (controller.signal.aborted) {
|
|
488
|
+
const terminal = gate.terminate(cancelledOutcome(""), now());
|
|
489
|
+
safeEmit({
|
|
490
|
+
type: "stream_cancelled",
|
|
491
|
+
runId,
|
|
492
|
+
timestamp: now(),
|
|
493
|
+
payload: { attemptedProviderIds }
|
|
494
|
+
});
|
|
495
|
+
if (terminal) yield terminal;
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const exception = createUnavailable("All eligible streaming providers failed.", {
|
|
500
|
+
runId,
|
|
501
|
+
details: { attemptedProviderIds }
|
|
502
|
+
});
|
|
503
|
+
const outcome: StreamTerminalOutcome = {
|
|
504
|
+
outcome: "error",
|
|
505
|
+
runId,
|
|
506
|
+
schemaVersion: "1.0",
|
|
507
|
+
error: exception.toContractError(),
|
|
508
|
+
partialText: ""
|
|
509
|
+
};
|
|
510
|
+
const terminal = gate.terminate(outcome, now());
|
|
511
|
+
safeEmit({
|
|
512
|
+
type: "stream_failed",
|
|
513
|
+
runId,
|
|
514
|
+
timestamp: now(),
|
|
515
|
+
payload: {
|
|
516
|
+
errorClass: exception.errorClass,
|
|
517
|
+
message: getStableMessage(exception.errorClass),
|
|
518
|
+
attemptedProviderIds
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
if (terminal) yield terminal;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return { handle, events: run(), cancel: (reason?: string) => controller.abort(reason) };
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Reports each registered provider's static descriptor and current dynamic capability check,
|
|
529
|
+
* without executing a task. Useful for UI that shows live provider availability before a run.
|
|
530
|
+
*/
|
|
531
|
+
async checkCapabilities(): Promise<ProviderCapabilitySnapshot[]> {
|
|
532
|
+
return Promise.all(
|
|
533
|
+
this.registry.list().map(async (provider) => ({
|
|
534
|
+
providerId: provider.describe().id,
|
|
535
|
+
descriptor: provider.describe(),
|
|
536
|
+
capabilities: await provider.capabilities(this.hostServices)
|
|
537
|
+
}))
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
}
|