@independo/inderun-web 0.4.0 → 0.5.0-rc.2
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/dist/core/artifacts.d.ts +110 -0
- package/dist/core/artifacts.d.ts.map +1 -0
- package/dist/core/artifacts.js +190 -0
- package/dist/core/engine.d.ts +28 -1
- package/dist/core/engine.d.ts.map +1 -1
- package/dist/core/engine.js +232 -28
- package/dist/core/generated/inderun-api.d.ts +12 -4
- 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/provenance.d.ts +5 -2
- package/dist/core/provenance.d.ts.map +1 -1
- package/dist/core/provenance.js +6 -2
- package/dist/core/provider.d.ts +50 -130
- package/dist/core/provider.d.ts.map +1 -1
- package/dist/core/route-planner.d.ts +5 -5
- package/dist/core/route-planner.d.ts.map +1 -1
- package/dist/core/route-planner.js +15 -29
- package/dist/core/router.d.ts +16 -0
- package/dist/core/router.d.ts.map +1 -1
- package/dist/core/router.js +14 -5
- package/dist/core/task-profiles.d.ts +54 -0
- package/dist/core/task-profiles.d.ts.map +1 -0
- package/dist/core/task-profiles.js +92 -0
- package/dist/facade/text.d.ts.map +1 -1
- package/dist/facade/text.js +5 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/providers/onnx/runtime.d.ts +2 -2
- package/dist/providers/onnx/runtime.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/core/artifacts.ts +280 -0
- package/src/core/engine.ts +281 -27
- package/src/core/generated/inderun-api.ts +12 -4
- package/src/core/host.ts +1 -0
- package/src/core/provenance.ts +7 -2
- package/src/core/provider.ts +64 -135
- package/src/core/route-planner.ts +20 -36
- package/src/core/router.ts +30 -7
- package/src/core/task-profiles.ts +145 -0
- package/src/facade/text.ts +5 -3
- package/src/index.ts +6 -1
- package/src/providers/onnx/runtime.ts +2 -2
package/src/core/engine.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type ArtifactReadRequest,
|
|
3
|
+
getArtifactReadRequestValidationIssues,
|
|
2
4
|
getTaskRequestValidationIssues,
|
|
5
|
+
type MediaArtifact,
|
|
3
6
|
type StreamEvent,
|
|
4
7
|
type StreamRunHandle,
|
|
5
8
|
type StreamTerminalOutcome,
|
|
9
|
+
type TaskOutput,
|
|
6
10
|
type TaskRequest,
|
|
7
11
|
type TaskResult
|
|
8
12
|
} from "@independo/inderun-contracts";
|
|
@@ -27,6 +31,18 @@ import {
|
|
|
27
31
|
validateModelProfiles
|
|
28
32
|
} from "./model-profiles.js";
|
|
29
33
|
import { ProvenanceRecorder } from "./provenance.js";
|
|
34
|
+
import {
|
|
35
|
+
checkOutput,
|
|
36
|
+
type OutputDelivery,
|
|
37
|
+
recheckBoundVoice,
|
|
38
|
+
taskProfileFor
|
|
39
|
+
} from "./task-profiles.js";
|
|
40
|
+
import {
|
|
41
|
+
type ArtifactChunk,
|
|
42
|
+
type ArtifactLimits,
|
|
43
|
+
ArtifactStore,
|
|
44
|
+
type AttemptArtifacts
|
|
45
|
+
} from "./artifacts.js";
|
|
30
46
|
|
|
31
47
|
/**
|
|
32
48
|
* @deprecated Renamed to `StreamRun`, which is the name the generated
|
|
@@ -49,6 +65,11 @@ export interface IndeRunOptions {
|
|
|
49
65
|
* `"auto"` is built in and cannot be redefined. See `docs/architecture/api-layering.md`.
|
|
50
66
|
*/
|
|
51
67
|
modelProfiles?: ModelProfiles;
|
|
68
|
+
/**
|
|
69
|
+
* Size limits of the engine's in-memory artifact store (ADR 017). Defaults: 16 MiB per
|
|
70
|
+
* artifact, 64 MiB in total, 1 MiB per `readArtifact` call.
|
|
71
|
+
*/
|
|
72
|
+
artifacts?: ArtifactLimits;
|
|
52
73
|
}
|
|
53
74
|
|
|
54
75
|
/**
|
|
@@ -58,6 +79,7 @@ export interface IndeRunOptions {
|
|
|
58
79
|
*/
|
|
59
80
|
export class IndeRun implements IndeRunApi {
|
|
60
81
|
private router: Router;
|
|
82
|
+
private readonly artifacts: ArtifactStore;
|
|
61
83
|
private telemetryService: TelemetryService;
|
|
62
84
|
private modelProfiles: ModelProfiles;
|
|
63
85
|
|
|
@@ -78,6 +100,7 @@ export class IndeRun implements IndeRunApi {
|
|
|
78
100
|
options.telemetryService || this.hostServices.telemetry || new NoOpTelemetryService();
|
|
79
101
|
this.modelProfiles = options.modelProfiles ?? {};
|
|
80
102
|
validateModelProfiles(this.modelProfiles);
|
|
103
|
+
this.artifacts = new ArtifactStore(options.artifacts);
|
|
81
104
|
}
|
|
82
105
|
|
|
83
106
|
/**
|
|
@@ -87,6 +110,12 @@ export class IndeRun implements IndeRunApi {
|
|
|
87
110
|
*/
|
|
88
111
|
private validateRequest(request: TaskRequest, runId: string): ResolvedModelProfile {
|
|
89
112
|
const validationIssues = getTaskRequestValidationIssues(request);
|
|
113
|
+
if (validationIssues.length === 0 && taskProfileFor(request.task.kind) === undefined) {
|
|
114
|
+
validationIssues.push({
|
|
115
|
+
path: "/task/kind",
|
|
116
|
+
message: `task kind "${request.task.kind}" is not supported by this engine`
|
|
117
|
+
});
|
|
118
|
+
}
|
|
90
119
|
if (validationIssues.length === 0) {
|
|
91
120
|
const resolved = resolveModelProfile(this.modelProfiles, request.modelProfile);
|
|
92
121
|
if (resolved.known) {
|
|
@@ -190,10 +219,13 @@ export class IndeRun implements IndeRunApi {
|
|
|
190
219
|
|
|
191
220
|
const runId = request.requestId || `run_${Math.random().toString(36).substring(2, 11)}`;
|
|
192
221
|
const provenance = new ProvenanceRecorder(request.modelProfile ?? AUTO_MODEL_PROFILE);
|
|
222
|
+
// The provider a failed run had committed to, which the failure is attributed to.
|
|
223
|
+
let committedProvider: ProviderAdapter | undefined;
|
|
193
224
|
|
|
194
225
|
try {
|
|
195
226
|
// 1. Validate request payload using schema contracts and resolve its model profile
|
|
196
227
|
const modelProfile = this.validateRequest(request, runId);
|
|
228
|
+
await this.checkDestination(request, runId);
|
|
197
229
|
|
|
198
230
|
// 2. Select the route based on constraints, model profile, and host capabilities
|
|
199
231
|
const routeSelection = await this.router.selectRoute(
|
|
@@ -204,21 +236,50 @@ export class IndeRun implements IndeRunApi {
|
|
|
204
236
|
);
|
|
205
237
|
const providers = [routeSelection.provider, ...routeSelection.fallbackProviders];
|
|
206
238
|
provenance.routed(routeSelection.routePlan);
|
|
239
|
+
const delivery = taskProfileFor(request.task.kind)!.delivery(request);
|
|
207
240
|
|
|
208
241
|
this.emitRouteDecided(runId, request, routeSelection, providers.length);
|
|
209
242
|
|
|
210
|
-
// 3. Execute the run task using the planned provider chain
|
|
243
|
+
// 3. Execute the run task using the planned provider chain. An attempt that committed
|
|
244
|
+
// (see RunContext.commit) ends the chain: its failure is the run's failure.
|
|
211
245
|
const attemptedProviderIds: string[] = [];
|
|
212
246
|
let lastError: unknown;
|
|
213
247
|
for (const [index, provider] of providers.entries()) {
|
|
214
248
|
const providerId = provider.describe().id;
|
|
249
|
+
const voice = routeSelection.candidates[index]?.voice;
|
|
215
250
|
attemptedProviderIds.push(providerId);
|
|
251
|
+
let committed = false;
|
|
252
|
+
// Closed when the attempt ends: a provider callback that fires after the run moved
|
|
253
|
+
// on must not be told it may start, say, audible playback.
|
|
254
|
+
let attemptOpen = true;
|
|
255
|
+
const commit = () => {
|
|
256
|
+
if (!attemptOpen) return false;
|
|
257
|
+
committed = true;
|
|
258
|
+
return true;
|
|
259
|
+
};
|
|
260
|
+
const attemptArtifacts = this.artifacts.attempt(commit);
|
|
261
|
+
let keptArtifactId: string | undefined;
|
|
216
262
|
|
|
217
263
|
try {
|
|
264
|
+
const refusal = recheckBoundVoice(request, providerId, voice);
|
|
265
|
+
if (refusal !== undefined) throw refusal;
|
|
266
|
+
|
|
218
267
|
const result = await provider.run(request, {
|
|
219
268
|
runId,
|
|
220
|
-
hostServices: this.hostServices
|
|
269
|
+
hostServices: this.hostServices,
|
|
270
|
+
...(voice !== undefined ? { voice } : {}),
|
|
271
|
+
commit,
|
|
272
|
+
artifacts: attemptArtifacts
|
|
221
273
|
});
|
|
274
|
+
result.output = this.normalizeOutput(
|
|
275
|
+
request,
|
|
276
|
+
delivery,
|
|
277
|
+
result.output,
|
|
278
|
+
providerId,
|
|
279
|
+
attemptArtifacts
|
|
280
|
+
);
|
|
281
|
+
result.output = await this.deliverToDestination(request, result.output, providerId);
|
|
282
|
+
keptArtifactId = engineOwnedArtifactId(result.output);
|
|
222
283
|
|
|
223
284
|
const endTime = this.now();
|
|
224
285
|
const totalMs = endTime - startTime;
|
|
@@ -229,7 +290,7 @@ export class IndeRun implements IndeRunApi {
|
|
|
229
290
|
providerUsed: providerId,
|
|
230
291
|
totalMs
|
|
231
292
|
};
|
|
232
|
-
provenance.attempted(providerId, "succeeded");
|
|
293
|
+
provenance.attempted(providerId, "succeeded", undefined, committed);
|
|
233
294
|
result.provenance = provenance.build(provider.describe(), result.telemetry.reportedModel);
|
|
234
295
|
|
|
235
296
|
this.safeEmit({
|
|
@@ -255,8 +316,15 @@ export class IndeRun implements IndeRunApi {
|
|
|
255
316
|
routePlan: routeSelection.routePlan
|
|
256
317
|
}
|
|
257
318
|
});
|
|
258
|
-
provenance.attempted(providerId, "failed", attemptError.errorClass);
|
|
319
|
+
provenance.attempted(providerId, "failed", attemptError.errorClass, committed);
|
|
259
320
|
lastError = attemptError;
|
|
321
|
+
if (committed) {
|
|
322
|
+
committedProvider = provider;
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
} finally {
|
|
326
|
+
attemptOpen = false;
|
|
327
|
+
attemptArtifacts.release(keptArtifactId);
|
|
260
328
|
}
|
|
261
329
|
}
|
|
262
330
|
|
|
@@ -285,8 +353,9 @@ export class IndeRun implements IndeRunApi {
|
|
|
285
353
|
});
|
|
286
354
|
// A routing refusal throws before any attempt; its rejections travel on the details.
|
|
287
355
|
provenance.routingFailed(normalized.details);
|
|
288
|
-
// No attempt produced the result, so the outcome is attributed to no provider
|
|
289
|
-
|
|
356
|
+
// No attempt produced the result, so the outcome is attributed to no provider -- unless
|
|
357
|
+
// the run had committed to one, which then owns the failure.
|
|
358
|
+
const exception = normalized.withProvenance(provenance.build(committedProvider?.describe()));
|
|
290
359
|
const endTime = this.now();
|
|
291
360
|
const totalMs =
|
|
292
361
|
typeof (exception as { details?: { totalMs?: number } }).details?.totalMs === "number"
|
|
@@ -336,6 +405,7 @@ export class IndeRun implements IndeRunApi {
|
|
|
336
405
|
let modelProfile: ResolvedModelProfile;
|
|
337
406
|
try {
|
|
338
407
|
modelProfile = this.validateRequest(request, runId);
|
|
408
|
+
await this.checkDestination(request, runId);
|
|
339
409
|
} catch (error) {
|
|
340
410
|
throw toIndeRunException(error, { runId }).withProvenance(provenance.build());
|
|
341
411
|
}
|
|
@@ -384,24 +454,40 @@ export class IndeRun implements IndeRunApi {
|
|
|
384
454
|
providerId: providers[0]!.describe().id
|
|
385
455
|
};
|
|
386
456
|
|
|
457
|
+
const delivery = taskProfileFor(request.task.kind)!.delivery(request);
|
|
458
|
+
const textRun = delivery === "inline";
|
|
459
|
+
const artifacts = this.artifacts;
|
|
460
|
+
const normalizeOutput = this.normalizeOutput.bind(this);
|
|
461
|
+
const deliverToDestination = this.deliverToDestination.bind(this);
|
|
462
|
+
|
|
387
463
|
async function* run(): AsyncGenerator<StreamEvent> {
|
|
388
464
|
const attemptedProviderIds: string[] = [];
|
|
465
|
+
// Set once, by whichever comes first in an attempt: the provider's commit(), its first
|
|
466
|
+
// admitted content event, or its result handoff. After it no other provider is tried.
|
|
389
467
|
let committed = false;
|
|
468
|
+
// Whether the committed attempt was managed playback that may have been heard.
|
|
469
|
+
let playbackCommitted = false;
|
|
390
470
|
|
|
391
471
|
const cancelledOutcome = (
|
|
392
472
|
partialText: string,
|
|
393
|
-
cancelledProvider?: ProviderAdapter
|
|
473
|
+
cancelledProvider?: ProviderAdapter,
|
|
474
|
+
delivered?: TaskOutput
|
|
394
475
|
): StreamTerminalOutcome => {
|
|
395
476
|
const reason =
|
|
396
477
|
typeof controller.signal.reason === "string" ? controller.signal.reason : undefined;
|
|
397
478
|
if (cancelledProvider !== undefined) {
|
|
398
|
-
provenance.attempted(cancelledProvider.describe().id, "cancelled");
|
|
479
|
+
provenance.attempted(cancelledProvider.describe().id, "cancelled", undefined, committed);
|
|
399
480
|
}
|
|
400
481
|
return {
|
|
401
482
|
outcome: "cancelled",
|
|
402
483
|
runId,
|
|
403
484
|
schemaVersion: "1.0",
|
|
404
|
-
|
|
485
|
+
// A text run reports what it delivered; committed playback reports that it played; an
|
|
486
|
+
// artifact already written to its destination is reported, since the application
|
|
487
|
+
// owns those bytes now. An uncommitted non-text run produced nothing to report.
|
|
488
|
+
...(textRun ? { partialText } : {}),
|
|
489
|
+
...(playbackCommitted ? { output: { type: "playback" as const, playback: {} } } : {}),
|
|
490
|
+
...(delivered !== undefined ? { output: delivered } : {}),
|
|
405
491
|
...(reason !== undefined ? { reason } : {}),
|
|
406
492
|
provenance: provenance.build(cancelledProvider?.describe())
|
|
407
493
|
};
|
|
@@ -413,6 +499,7 @@ export class IndeRun implements IndeRunApi {
|
|
|
413
499
|
}
|
|
414
500
|
|
|
415
501
|
const providerId = provider.describe().id;
|
|
502
|
+
const voice = routeSelection.candidates[index]?.voice;
|
|
416
503
|
attemptedProviderIds.push(providerId);
|
|
417
504
|
safeEmit({
|
|
418
505
|
type: "stream_attempt_started",
|
|
@@ -421,12 +508,41 @@ export class IndeRun implements IndeRunApi {
|
|
|
421
508
|
payload: { providerId, fallbackOccurred: index > 0 }
|
|
422
509
|
});
|
|
423
510
|
|
|
511
|
+
const markCommitted = () => {
|
|
512
|
+
if (committed) return;
|
|
513
|
+
committed = true;
|
|
514
|
+
playbackCommitted = delivery === "playback";
|
|
515
|
+
safeEmit({
|
|
516
|
+
type: "stream_attempt_succeeded",
|
|
517
|
+
runId,
|
|
518
|
+
timestamp: now(),
|
|
519
|
+
payload: { providerId, attemptedProviderIds }
|
|
520
|
+
});
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// Closed when the attempt ends. A provider callback that fires after the run fell back
|
|
524
|
+
// must neither start audio nor commit the attempt that replaced this one.
|
|
525
|
+
let attemptOpen = true;
|
|
526
|
+
const commit = () => {
|
|
527
|
+
if (!attemptOpen || controller.signal.aborted || gate.isTerminated()) return false;
|
|
528
|
+
markCommitted();
|
|
529
|
+
return true;
|
|
530
|
+
};
|
|
531
|
+
const attemptArtifacts = artifacts.attempt(commit);
|
|
532
|
+
let keptArtifactId: string | undefined;
|
|
533
|
+
|
|
424
534
|
let partialText = "";
|
|
425
535
|
try {
|
|
536
|
+
const refusal = recheckBoundVoice(request, providerId, voice);
|
|
537
|
+
if (refusal !== undefined) throw refusal;
|
|
538
|
+
|
|
426
539
|
const iterable = (provider as Required<Pick<ProviderAdapter, "stream">>).stream(request, {
|
|
427
540
|
runId,
|
|
428
541
|
hostServices: hostServices,
|
|
429
|
-
signal: controller.signal
|
|
542
|
+
signal: controller.signal,
|
|
543
|
+
...(voice !== undefined ? { voice } : {}),
|
|
544
|
+
commit,
|
|
545
|
+
artifacts: attemptArtifacts
|
|
430
546
|
});
|
|
431
547
|
|
|
432
548
|
let terminatedInLoop = false;
|
|
@@ -440,16 +556,12 @@ export class IndeRun implements IndeRunApi {
|
|
|
440
556
|
}
|
|
441
557
|
|
|
442
558
|
if (ev.kind === "delta" || ev.kind === "snapshot") {
|
|
443
|
-
|
|
444
|
-
if (!
|
|
445
|
-
|
|
446
|
-
safeEmit({
|
|
447
|
-
type: "stream_attempt_succeeded",
|
|
448
|
-
runId,
|
|
449
|
-
timestamp: now(),
|
|
450
|
-
payload: { providerId, attemptedProviderIds }
|
|
451
|
-
});
|
|
559
|
+
// Content events are text-only (ADR 016 §5).
|
|
560
|
+
if (!textRun) {
|
|
561
|
+
throw checkOutput(request, delivery, { type: "text", text: "" }, providerId)!;
|
|
452
562
|
}
|
|
563
|
+
partialText = ev.kind === "delta" ? partialText + ev.text : ev.text;
|
|
564
|
+
markCommitted();
|
|
453
565
|
const streamEvent = gate.admit({
|
|
454
566
|
timestamp: now(),
|
|
455
567
|
type: ev.kind === "delta" ? "content_delta" : "content_snapshot",
|
|
@@ -459,16 +571,49 @@ export class IndeRun implements IndeRunApi {
|
|
|
459
571
|
continue;
|
|
460
572
|
}
|
|
461
573
|
|
|
462
|
-
// ev.kind === "done"
|
|
463
|
-
|
|
574
|
+
// ev.kind === "done" (text) or "result" (any task): the run's final output.
|
|
575
|
+
const output = normalizeOutput(
|
|
576
|
+
request,
|
|
577
|
+
delivery,
|
|
578
|
+
ev.kind === "done" ? { type: "text" as const, text: ev.finalText } : ev.output,
|
|
579
|
+
providerId,
|
|
580
|
+
attemptArtifacts
|
|
581
|
+
);
|
|
582
|
+
markCommitted();
|
|
583
|
+
const delivered = await deliverToDestination(request, output, providerId);
|
|
584
|
+
// A cancel that landed during the destination write did not interrupt it, but the
|
|
585
|
+
// run still ends cancelled. Bytes already at the destination are the application's,
|
|
586
|
+
// so the outcome reports them; anything else the usual cancel path below releases.
|
|
587
|
+
if (controller.signal.aborted) {
|
|
588
|
+
if (delivered.artifact?.owner !== "application") break;
|
|
589
|
+
const terminal = gate.terminate(
|
|
590
|
+
cancelledOutcome(partialText, provider, delivered),
|
|
591
|
+
now()
|
|
592
|
+
);
|
|
593
|
+
safeEmit({
|
|
594
|
+
type: "stream_cancelled",
|
|
595
|
+
runId,
|
|
596
|
+
timestamp: now(),
|
|
597
|
+
payload: { providerId, attemptedProviderIds }
|
|
598
|
+
});
|
|
599
|
+
terminatedInLoop = true;
|
|
600
|
+
if (terminal) yield terminal;
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
keptArtifactId = engineOwnedArtifactId(delivered);
|
|
604
|
+
provenance.attempted(providerId, "succeeded", undefined, committed);
|
|
464
605
|
const outcome: StreamTerminalOutcome = {
|
|
465
606
|
outcome: "completed",
|
|
466
607
|
runId,
|
|
467
608
|
schemaVersion: "1.0",
|
|
468
|
-
|
|
609
|
+
...(ev.kind === "done"
|
|
610
|
+
? { finalText: ev.finalText }
|
|
611
|
+
: output.type === "text"
|
|
612
|
+
? { finalText: output.text!, output }
|
|
613
|
+
: { output }),
|
|
469
614
|
telemetry: { providerUsed: providerId, totalMs: now() - startTime },
|
|
470
615
|
...(ev.finishReason !== undefined ? { finishReason: ev.finishReason } : {}),
|
|
471
|
-
...(ev.usage !== undefined ? { usage: ev.usage } : {}),
|
|
616
|
+
...(ev.kind === "done" && ev.usage !== undefined ? { usage: ev.usage } : {}),
|
|
472
617
|
provenance: provenance.build(provider.describe(), ev.reportedModel)
|
|
473
618
|
};
|
|
474
619
|
const terminal = gate.terminate(outcome, now());
|
|
@@ -488,6 +633,7 @@ export class IndeRun implements IndeRunApi {
|
|
|
488
633
|
}
|
|
489
634
|
|
|
490
635
|
if (controller.signal.aborted) {
|
|
636
|
+
attemptArtifacts.release();
|
|
491
637
|
const terminal = gate.terminate(cancelledOutcome(partialText, provider), now());
|
|
492
638
|
safeEmit({
|
|
493
639
|
type: "stream_cancelled",
|
|
@@ -499,10 +645,11 @@ export class IndeRun implements IndeRunApi {
|
|
|
499
645
|
return;
|
|
500
646
|
}
|
|
501
647
|
|
|
502
|
-
// Provider's iterable ended without a terminal "done"/"error" event.
|
|
648
|
+
// Provider's iterable ended without a terminal "done"/"result"/"error" event.
|
|
503
649
|
throw new Error(`Provider '${providerId}' stream ended without a terminal event.`);
|
|
504
650
|
} catch (err) {
|
|
505
651
|
if (controller.signal.aborted) {
|
|
652
|
+
attemptArtifacts.release();
|
|
506
653
|
const terminal = gate.terminate(cancelledOutcome(partialText, provider), now());
|
|
507
654
|
safeEmit({
|
|
508
655
|
type: "stream_cancelled",
|
|
@@ -519,7 +666,7 @@ export class IndeRun implements IndeRunApi {
|
|
|
519
666
|
runId,
|
|
520
667
|
details: { attemptedProviderIds }
|
|
521
668
|
});
|
|
522
|
-
provenance.attempted(providerId, "failed", exception.errorClass);
|
|
669
|
+
provenance.attempted(providerId, "failed", exception.errorClass, committed);
|
|
523
670
|
|
|
524
671
|
if (!committed) {
|
|
525
672
|
safeEmit({
|
|
@@ -536,12 +683,13 @@ export class IndeRun implements IndeRunApi {
|
|
|
536
683
|
}
|
|
537
684
|
|
|
538
685
|
// Committed: the failure belongs to this provider, not to the chain.
|
|
686
|
+
attemptArtifacts.release();
|
|
539
687
|
const outcome: StreamTerminalOutcome = {
|
|
540
688
|
outcome: "error",
|
|
541
689
|
runId,
|
|
542
690
|
schemaVersion: "1.0",
|
|
543
691
|
error: exception.toContractError(),
|
|
544
|
-
partialText,
|
|
692
|
+
...(textRun ? { partialText } : {}),
|
|
545
693
|
provenance: provenance.build(provider.describe())
|
|
546
694
|
};
|
|
547
695
|
const terminal = gate.terminate(outcome, now());
|
|
@@ -558,6 +706,9 @@ export class IndeRun implements IndeRunApi {
|
|
|
558
706
|
});
|
|
559
707
|
if (terminal) yield terminal;
|
|
560
708
|
return;
|
|
709
|
+
} finally {
|
|
710
|
+
attemptOpen = false;
|
|
711
|
+
attemptArtifacts.release(keptArtifactId);
|
|
561
712
|
}
|
|
562
713
|
}
|
|
563
714
|
|
|
@@ -583,7 +734,7 @@ export class IndeRun implements IndeRunApi {
|
|
|
583
734
|
runId,
|
|
584
735
|
schemaVersion: "1.0",
|
|
585
736
|
error: exception.toContractError(),
|
|
586
|
-
partialText: "",
|
|
737
|
+
...(textRun ? { partialText: "" } : {}),
|
|
587
738
|
provenance: provenance.build()
|
|
588
739
|
};
|
|
589
740
|
const terminal = gate.terminate(outcome, now());
|
|
@@ -607,6 +758,104 @@ export class IndeRun implements IndeRunApi {
|
|
|
607
758
|
* Reports each registered provider's static descriptor and current dynamic capability check,
|
|
608
759
|
* without executing a task. Useful for UI that shows live provider availability before a run.
|
|
609
760
|
*/
|
|
761
|
+
async readArtifact(request: ArtifactReadRequest): Promise<ArtifactChunk> {
|
|
762
|
+
const validationIssues = getArtifactReadRequestValidationIssues(request);
|
|
763
|
+
if (validationIssues.length > 0) {
|
|
764
|
+
const message = `Validation failed for ArtifactReadRequest: ${validationIssues
|
|
765
|
+
.map((i) => `${i.path} - ${i.message}`)
|
|
766
|
+
.join("; ")}`;
|
|
767
|
+
throw createInternal(message, { details: { validationIssues } });
|
|
768
|
+
}
|
|
769
|
+
return this.artifacts.read(request);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async disposeArtifact(artifact: MediaArtifact): Promise<void> {
|
|
773
|
+
if (artifact.owner === "engine") {
|
|
774
|
+
this.artifacts.dispose(artifact.id);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Refuses, before routing, an artifact request whose `destinationRef` this host cannot
|
|
780
|
+
* write to: there is no point running a provider whose result has nowhere to go.
|
|
781
|
+
*/
|
|
782
|
+
private async checkDestination(request: TaskRequest, runId: string): Promise<void> {
|
|
783
|
+
const destinationRef = request.delivery?.destinationRef;
|
|
784
|
+
if (destinationRef === undefined) return;
|
|
785
|
+
const destinations = this.hostServices.artifactDestinations;
|
|
786
|
+
if (destinations === undefined) {
|
|
787
|
+
throw createCapabilityMismatch("This host has no application artifact destinations.", {
|
|
788
|
+
runId,
|
|
789
|
+
details: { code: "artifact_destination_unsupported" }
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
if (!(await destinations.accepts(destinationRef, request.delivery?.mediaTypes))) {
|
|
793
|
+
throw createCapabilityMismatch("The host refused the requested artifact destination.", {
|
|
794
|
+
runId,
|
|
795
|
+
details: { code: "artifact_destination_refused" }
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
/**
|
|
801
|
+
* Writes a finished artifact to the request's application destination, if it names one,
|
|
802
|
+
* and drops the engine's copy: the result's artifact is then application-owned. The run
|
|
803
|
+
* has committed by now, so a failed write is the run's failure, never a fallback.
|
|
804
|
+
*/
|
|
805
|
+
private async deliverToDestination(
|
|
806
|
+
request: TaskRequest,
|
|
807
|
+
output: TaskOutput,
|
|
808
|
+
providerId: string
|
|
809
|
+
): Promise<TaskOutput> {
|
|
810
|
+
const destinationRef = request.delivery?.destinationRef;
|
|
811
|
+
const artifact = output.artifact;
|
|
812
|
+
if (destinationRef === undefined || artifact === undefined) return output;
|
|
813
|
+
try {
|
|
814
|
+
await this.hostServices.artifactDestinations!.write(
|
|
815
|
+
destinationRef,
|
|
816
|
+
artifact,
|
|
817
|
+
this.artifacts.bytes(artifact.id)!
|
|
818
|
+
);
|
|
819
|
+
} catch {
|
|
820
|
+
// The host's error stays out of the run's error: it may name the destination.
|
|
821
|
+
this.artifacts.dispose(artifact.id);
|
|
822
|
+
throw createInternal("Writing the artifact to its application destination failed.", {
|
|
823
|
+
providerId,
|
|
824
|
+
details: { code: "artifact_destination_write_failed" }
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
this.artifacts.dispose(artifact.id);
|
|
828
|
+
return { ...output, artifact: { ...artifact, owner: "application", destinationRef } };
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/**
|
|
832
|
+
* Checks that an artifact in a provider's output is one this attempt produced, swaps in the
|
|
833
|
+
* reference the store holds, and only then checks the output against the requested delivery:
|
|
834
|
+
* the artifact's media type, size and ownership are the engine's record rather than the
|
|
835
|
+
* provider's claim.
|
|
836
|
+
*/
|
|
837
|
+
private normalizeOutput(
|
|
838
|
+
request: TaskRequest,
|
|
839
|
+
delivery: OutputDelivery,
|
|
840
|
+
output: TaskOutput | undefined,
|
|
841
|
+
providerId: string,
|
|
842
|
+
attemptArtifacts: AttemptArtifacts
|
|
843
|
+
): TaskOutput {
|
|
844
|
+
let resolved = output;
|
|
845
|
+
if (output?.artifact !== undefined) {
|
|
846
|
+
if (!attemptArtifacts.owns(output.artifact.id)) {
|
|
847
|
+
throw createInternal(
|
|
848
|
+
`Provider '${providerId}' returned an artifact it did not hand over in this attempt.`,
|
|
849
|
+
{ providerId, details: { code: "output_mismatch" } }
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
resolved = { ...output, artifact: this.artifacts.reference(output.artifact.id)! };
|
|
853
|
+
}
|
|
854
|
+
const mismatch = checkOutput(request, delivery, resolved, providerId);
|
|
855
|
+
if (mismatch !== undefined) throw mismatch;
|
|
856
|
+
return resolved!;
|
|
857
|
+
}
|
|
858
|
+
|
|
610
859
|
async checkCapabilities(): Promise<ProviderCapabilitySnapshot[]> {
|
|
611
860
|
return Promise.all(
|
|
612
861
|
this.registry.list().map(async (provider) => ({
|
|
@@ -617,3 +866,8 @@ export class IndeRun implements IndeRunApi {
|
|
|
617
866
|
);
|
|
618
867
|
}
|
|
619
868
|
}
|
|
869
|
+
|
|
870
|
+
/** The id of an artifact output the engine still holds, which an attempt must keep. */
|
|
871
|
+
function engineOwnedArtifactId(output: TaskOutput): string | undefined {
|
|
872
|
+
return output.artifact?.owner === "engine" ? output.artifact.id : undefined;
|
|
873
|
+
}
|
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
/* This file was generated from contracts/api/inderun-api.json. Do not edit by hand. */
|
|
2
2
|
|
|
3
|
-
import type { TaskRequest, TaskResult } from "@independo/inderun-contracts";
|
|
4
|
-
import type { StreamRun,
|
|
3
|
+
import type { TaskRequest, TaskResult, ProviderCapabilitySnapshot, ArtifactReadRequest, MediaArtifact } from "@independo/inderun-contracts";
|
|
4
|
+
import type { StreamRun, ArtifactChunk } from "../provider.js";
|
|
5
5
|
|
|
6
6
|
export interface IndeRunApi {
|
|
7
7
|
/**
|
|
8
|
-
* Executes a Mode 1 (request/response)
|
|
8
|
+
* Executes a Mode 1 (request/response) task of any supported kind against a routed provider and returns the completed TaskResult, whose output is text, a managed-playback completion, or a media artifact as the request's delivery asks. Routing (local vs. cloud, which provider) is decided internally from TaskRequest.constraints/preferences — the caller does not select a provider directly. Rejects the request up front on validation failure; otherwise throws an IndeRunException (carrying an IndeRunError) if no eligible provider is found or every attempted provider fails. There is no partial or streaming result in this mode: run() either resolves with a complete TaskResult or throws. A provider failure falls back to the next planned provider unless the attempt had committed (for managed playback, no later than its first possible audible effect); a committed attempt's failure is the run's failure.
|
|
9
9
|
*/
|
|
10
10
|
run(request: TaskRequest): Promise<TaskResult>;
|
|
11
11
|
/**
|
|
12
|
-
* Executes a Mode 2 (streaming)
|
|
12
|
+
* Executes a Mode 2 (streaming) task of any supported kind and returns a handle to the run, its canonical StreamEvent sequence, and a cancel hook. The requested interaction mode is a routing input rather than a filter applied after routing: providers that cannot stream are rejected during planning with a normalized reason, so a stream and a run over the same registry may legitimately resolve to different provider chains. Two failure surfaces exist deliberately — validation and route-selection failures reject this call, since there is no handle to correlate them with yet, while provider failure, cancellation, and completion all arrive as the single terminal StreamEvent in the returned sequence. Fallback is narrower than run()'s: a provider failure is only fallback-eligible before the run commits to that provider -- its first content event, its explicit commit (managed playback, before the first possible audible effect), or its result handoff, whichever comes first; once committed, a later failure becomes a terminal error rather than a silent provider swap. Cancellation forecloses both at any commit state and always produces exactly one cancelled outcome.
|
|
13
13
|
*/
|
|
14
14
|
stream(request: TaskRequest): Promise<StreamRun>;
|
|
15
15
|
/**
|
|
16
16
|
* Returns a live snapshot of every registered provider's static descriptor and current dynamic availability, without executing a task or producing any side effects. Call this before run() when you need to show which providers/models are currently available — for example a settings screen or provider picker. Availability can change between calls (a local model can unload, cloud credentials can expire), so don't cache the result across a run() call.
|
|
17
17
|
*/
|
|
18
18
|
checkCapabilities(): Promise<ProviderCapabilitySnapshot[]>;
|
|
19
|
+
/**
|
|
20
|
+
* Reads a byte range of an engine-owned media artifact a run produced (ADR 017): the bytes in the platform's native byte type, with their offset, the artifact's total size, and whether the range reaches its end. Reads are repeatable until the artifact is disposed, and each returns at most the engine's configured maximum read size. Throws an IndeRunException with errorClass Unavailable and details.code 'artifact_not_found' for an unknown, disposed or application-owned artifact, and a validation error for an offset beyond the end.
|
|
21
|
+
*/
|
|
22
|
+
readArtifact(request: ArtifactReadRequest): Promise<ArtifactChunk>;
|
|
23
|
+
/**
|
|
24
|
+
* Releases the bytes of an engine-owned media artifact. Idempotent: disposing an artifact twice, or one the engine does not hold, does nothing. After disposal readArtifact reports 'artifact_not_found'. An artifact outlives the run that produced it, so an application that keeps none should dispose each one it receives.
|
|
25
|
+
*/
|
|
26
|
+
disposeArtifact(artifact: MediaArtifact): Promise<void>;
|
|
19
27
|
}
|
package/src/core/host.ts
CHANGED
package/src/core/provenance.ts
CHANGED
|
@@ -35,15 +35,20 @@ export class ProvenanceRecorder {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Records how one provider attempt ended, and whether the run had committed to it
|
|
40
|
+
* (first content delivered, first possible audible effect, or an artifact handed over).
|
|
41
|
+
*/
|
|
39
42
|
attempted(
|
|
40
43
|
providerId: string,
|
|
41
44
|
outcome: ExecutionProvenanceAttempt["outcome"],
|
|
42
|
-
errorClass?: IndeRunErrorClass
|
|
45
|
+
errorClass?: IndeRunErrorClass,
|
|
46
|
+
committed = false
|
|
43
47
|
): void {
|
|
44
48
|
this.attempts.push({
|
|
45
49
|
providerId,
|
|
46
50
|
outcome,
|
|
51
|
+
...(committed ? { committed: true } : {}),
|
|
47
52
|
...(errorClass !== undefined ? { errorClass } : {})
|
|
48
53
|
});
|
|
49
54
|
}
|