@independo/inderun-web 0.5.0-rc.1 → 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.
Files changed (39) hide show
  1. package/dist/core/artifacts.d.ts +110 -0
  2. package/dist/core/artifacts.d.ts.map +1 -0
  3. package/dist/core/artifacts.js +190 -0
  4. package/dist/core/engine.d.ts +28 -1
  5. package/dist/core/engine.d.ts.map +1 -1
  6. package/dist/core/engine.js +232 -28
  7. package/dist/core/generated/inderun-api.d.ts +12 -4
  8. package/dist/core/generated/inderun-api.d.ts.map +1 -1
  9. package/dist/core/host.d.ts +1 -1
  10. package/dist/core/host.d.ts.map +1 -1
  11. package/dist/core/provenance.d.ts +5 -2
  12. package/dist/core/provenance.d.ts.map +1 -1
  13. package/dist/core/provenance.js +6 -2
  14. package/dist/core/provider.d.ts +44 -1
  15. package/dist/core/provider.d.ts.map +1 -1
  16. package/dist/core/route-planner.d.ts.map +1 -1
  17. package/dist/core/route-planner.js +5 -6
  18. package/dist/core/router.d.ts +16 -0
  19. package/dist/core/router.d.ts.map +1 -1
  20. package/dist/core/router.js +14 -5
  21. package/dist/core/task-profiles.d.ts +54 -0
  22. package/dist/core/task-profiles.d.ts.map +1 -0
  23. package/dist/core/task-profiles.js +92 -0
  24. package/dist/facade/text.d.ts.map +1 -1
  25. package/dist/facade/text.js +5 -3
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/package.json +3 -3
  29. package/src/core/artifacts.ts +280 -0
  30. package/src/core/engine.ts +281 -27
  31. package/src/core/generated/inderun-api.ts +12 -4
  32. package/src/core/host.ts +1 -0
  33. package/src/core/provenance.ts +7 -2
  34. package/src/core/provider.ts +53 -0
  35. package/src/core/route-planner.ts +5 -6
  36. package/src/core/router.ts +30 -7
  37. package/src/core/task-profiles.ts +145 -0
  38. package/src/facade/text.ts +5 -3
  39. package/src/index.ts +6 -1
@@ -1,3 +1,4 @@
1
+ import { taskProfileFor } from "./task-profiles.js";
1
2
  /**
2
3
  * {@link RoutePlanner} backed by the Rust route core compiled to WASM. The
3
4
  * module specifier is a static literal (not configurable) so bundlers such as
@@ -117,14 +118,12 @@ export function buildSharedPlannerInput(request, snapshots, networkOnline, inter
117
118
  };
118
119
  }
119
120
  /**
120
- * Projects the request's task onto the requirements the planner can check. A text
121
- * request carries text input; the speech requirements arrive with the speech request
122
- * shape (#245).
121
+ * Projects the request's task onto the requirements the planner can check, through the
122
+ * kind's task profile. A kind without a profile carries the kind alone, which the planner
123
+ * matches against declarations and nothing else.
123
124
  */
124
125
  function plannerTask(request) {
125
- return request.task.kind === "text_to_text"
126
- ? { kind: request.task.kind, inputs: ["text"] }
127
- : { kind: request.task.kind };
126
+ return taskProfileFor(request.task.kind)?.plannerTask(request) ?? { kind: request.task.kind };
128
127
  }
129
128
  /**
130
129
  * Projects the dynamic capability snapshot for the planner.
@@ -2,7 +2,19 @@ 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 { BoundVoice } from "./task-profiles.js";
5
6
  import { type InteractionMode, type RoutePlanner, type SharedPlannerInput, type SharedPlannerRoutePlan } from "./route-planner.js";
7
+ /**
8
+ * One provider in the planned chain, with the voice the plan bound it to (speech only).
9
+ */
10
+ export interface RouteCandidate {
11
+ provider: ProviderAdapter;
12
+ /**
13
+ * The voice this provider must use, chosen by the planner within the request's privacy
14
+ * policy. Absent when the task has no voice or the provider does not list its voices.
15
+ */
16
+ voice?: BoundVoice;
17
+ }
6
18
  /**
7
19
  * Output structure from the Router containing the chosen provider and a route selection description.
8
20
  */
@@ -15,6 +27,10 @@ export interface RouteSelection {
15
27
  * Ordered fallback providers to try if the primary provider fails before producing a final result.
16
28
  */
17
29
  fallbackProviders: ProviderAdapter[];
30
+ /**
31
+ * The whole chain in attempt order (`provider` first), each with its bound voice.
32
+ */
33
+ candidates: RouteCandidate[];
18
34
  /**
19
35
  * The shared route plan used to select this provider chain.
20
36
  */
@@ -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,EAGL,KAAK,eAAe,EAEpB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAE5B,MAAM,oBAAoB,CAAC;AAE5B;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;OAEG;IACH,QAAQ,EAAE,eAAe,CAAC;IAC1B;;OAEG;IACH,iBAAiB,EAAE,eAAe,EAAE,CAAC;IACrC;;OAEG;IACH,SAAS,EAAE,sBAAsB,CAAC;IAClC;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,qBAAa,MAAM;IAEf,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,OAAO;gBADP,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,GAAE,YAAqC;IAGxD;;;;;;;;;;;;;;OAcG;IACG,WAAW,CACf,OAAO,EAAE,WAAW,EACpB,YAAY,EAAE,YAAY,EAC1B,eAAe,GAAE,eAAuB,EACxC,YAAY,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,GAChD,OAAO,CAAC,cAAc,CAAC;IA0B1B,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,2BAA2B;IAuBnC;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;CAkBzB"}
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;AACtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAOrD,OAAO,EAGL,KAAK,eAAe,EAEpB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAE5B,MAAM,oBAAoB,CAAC;AAE5B;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,eAAe,CAAC;IAC1B;;;OAGG;IACH,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B;;OAEG;IACH,QAAQ,EAAE,eAAe,CAAC;IAC1B;;OAEG;IACH,iBAAiB,EAAE,eAAe,EAAE,CAAC;IACrC;;OAEG;IACH,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B;;OAEG;IACH,SAAS,EAAE,sBAAsB,CAAC;IAClC;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;GAIG;AACH,qBAAa,MAAM;IAEf,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,OAAO;gBADP,QAAQ,EAAE,gBAAgB,EAC1B,OAAO,GAAE,YAAqC;IAGxD;;;;;;;;;;;;;;OAcG;IACG,WAAW,CACf,OAAO,EAAE,WAAW,EACpB,YAAY,EAAE,YAAY,EAC1B,eAAe,GAAE,eAAuB,EACxC,YAAY,CAAC,EAAE,kBAAkB,CAAC,cAAc,CAAC,GAChD,OAAO,CAAC,cAAc,CAAC;IA0B1B,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,2BAA2B;IA6BnC;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;CAkBzB"}
@@ -50,16 +50,25 @@ export class Router {
50
50
  return this.buildSelectionFromRoutePlan(snapshots, routePlan);
51
51
  }
52
52
  buildSelectionFromRoutePlan(snapshots, routePlan) {
53
- const orderedProviders = routePlan.candidates
54
- .map((candidate) => snapshots.find((snapshot) => snapshot.descriptor.id === candidate.providerId))
55
- .filter((snapshot) => snapshot !== undefined);
56
- const selected = orderedProviders[0];
53
+ const candidates = routePlan.candidates.flatMap((candidate) => {
54
+ const snapshot = snapshots.find((s) => s.descriptor.id === candidate.providerId);
55
+ if (snapshot === undefined)
56
+ return [];
57
+ return [
58
+ {
59
+ provider: snapshot.provider,
60
+ ...(candidate.voice !== undefined ? { voice: candidate.voice } : {})
61
+ }
62
+ ];
63
+ });
64
+ const selected = candidates[0];
57
65
  if (!selected) {
58
66
  throw createInternal("Route plan selected a provider that is no longer registered.");
59
67
  }
60
68
  return {
61
69
  provider: selected.provider,
62
- fallbackProviders: orderedProviders.slice(1).map((snapshot) => snapshot.provider),
70
+ fallbackProviders: candidates.slice(1).map((candidate) => candidate.provider),
71
+ candidates,
63
72
  routePlan,
64
73
  explanation: routePlan.explanation.summary
65
74
  };
@@ -0,0 +1,54 @@
1
+ import type { SharedRoutePlan, SharedRoutePlannerInput, TaskOutput, TaskRequest } from "@independo/inderun-contracts";
2
+ import { type IndeRunException } from "./errors.js";
3
+ /** How a task's result reaches the application: `inline`, `playback` or `artifact`. */
4
+ export type OutputDelivery = NonNullable<TaskRequest["delivery"]>["mode"];
5
+ /** The provider-neutral voice a route candidate is bound to. */
6
+ export type BoundVoice = NonNullable<SharedRoutePlan["candidates"][number]["voice"]>;
7
+ /**
8
+ * What the engine needs to know about one task kind (ADR 016). This table is the only
9
+ * place the engine branches on `task.kind`: which kinds it executes, the projection onto
10
+ * the planner's task requirements, and the output delivery a result must have. (Per-kind
11
+ * request requirements are the schema's `if`/`then` branches, checked by the validator.) Everything else — routing, fallback, cancellation, provenance — is task-agnostic,
12
+ * so a new task adds an entry here rather than a second execution path.
13
+ *
14
+ * The Swift (`TaskProfiles.swift`) and Kotlin (`TaskProfiles.kt`) engines keep the same
15
+ * table; `contracts/fixtures/task-contracts/` holds the planner projection all three must
16
+ * produce for each request.
17
+ */
18
+ export interface TaskProfile {
19
+ /** The `task.kind` this profile describes. */
20
+ readonly kind: string;
21
+ /** Projects the request onto the requirements the shared planner can check. */
22
+ plannerTask(request: TaskRequest): SharedRoutePlannerInput["task"];
23
+ /** The output delivery the request asks for: `inline` for text, else `delivery.mode`. */
24
+ delivery(request: TaskRequest): OutputDelivery;
25
+ }
26
+ /**
27
+ * Adds a task profile. Internal and not exported from the package: a real task needs its
28
+ * schema kind and payload first (ADR 016). It exists so a test can prove a third task runs
29
+ * through routing, execution and output normalization without changing the engine.
30
+ * @internal
31
+ */
32
+ export declare function registerTaskProfile(profile: TaskProfile): void;
33
+ /** The profile for `kind`, or `undefined` when this engine cannot execute the task. */
34
+ export declare function taskProfileFor(kind: string): TaskProfile | undefined;
35
+ /**
36
+ * Re-checks, immediately before an attempt, that the voice the plan bound for this
37
+ * provider still satisfies the request's privacy policy. The planner already chose the
38
+ * voice inside that policy; repeating the check here keeps privacy enforceable at the
39
+ * last point before data reaches the provider, independent of how the plan was produced.
40
+ * A violation fails the attempt before commit, so the run may fall back.
41
+ *
42
+ * @returns The exception to fail the attempt with, or `undefined` when the voice is allowed.
43
+ */
44
+ export declare function recheckBoundVoice(request: TaskRequest, providerId: string, voice: BoundVoice | undefined): IndeRunException | undefined;
45
+ /**
46
+ * Checks a provider's output against the delivery the request asked for: inline text for
47
+ * `inline`, a playback completion for `playback`, a media artifact for `artifact` whose
48
+ * media type is one the request accepts. A provider never substitutes one for another
49
+ * (ADR 016 §4), so a mismatch is a provider fault, not a result.
50
+ *
51
+ * @returns The exception to fail the attempt with, or `undefined` when the output matches.
52
+ */
53
+ export declare function checkOutput(request: TaskRequest, delivery: OutputDelivery, output: TaskOutput | undefined, providerId: string): IndeRunException | undefined;
54
+ //# sourceMappingURL=task-profiles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"task-profiles.d.ts","sourceRoot":"","sources":["../../src/core/task-profiles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,uBAAuB,EACvB,UAAU,EACV,WAAW,EACZ,MAAM,8BAA8B,CAAC;AACtC,OAAO,EAA4C,KAAK,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE9F,uFAAuF;AACvF,MAAM,MAAM,cAAc,GAAG,WAAW,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC;AAC1E,gEAAgE;AAChE,MAAM,MAAM,UAAU,GAAG,WAAW,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC;AAErF;;;;;;;;;;GAUG;AACH,MAAM,WAAW,WAAW;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,WAAW,CAAC,OAAO,EAAE,WAAW,GAAG,uBAAuB,CAAC,MAAM,CAAC,CAAC;IACnE,yFAAyF;IACzF,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,cAAc,CAAC;CAChD;AAwCD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAE9D;AAED,uFAAuF;AACvF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,SAAS,CAEpE;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,WAAW,EACpB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,UAAU,GAAG,SAAS,GAC5B,gBAAgB,GAAG,SAAS,CAQ9B;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,cAAc,EACxB,MAAM,EAAE,UAAU,GAAG,SAAS,EAC9B,UAAU,EAAE,MAAM,GACjB,gBAAgB,GAAG,SAAS,CAc9B"}
@@ -0,0 +1,92 @@
1
+ import { createCapabilityMismatch, createInternal } from "./errors.js";
2
+ const textToText = {
3
+ kind: "text_to_text",
4
+ plannerTask: () => ({ kind: "text_to_text", inputs: ["text"] }),
5
+ delivery: () => "inline"
6
+ };
7
+ const textToSpeech = {
8
+ kind: "text_to_speech",
9
+ plannerTask: (request) => {
10
+ // The schema requires both for this kind; validation runs before projection.
11
+ const speech = request.speech;
12
+ const delivery = request.delivery;
13
+ return {
14
+ kind: "text_to_speech",
15
+ inputs: ["text"],
16
+ delivery: {
17
+ mode: delivery.mode,
18
+ ...(delivery.mediaTypes !== undefined ? { mediaTypes: [...delivery.mediaTypes] } : {})
19
+ },
20
+ locale: { tag: speech.locale, match: speech.localeMatch ?? "language" },
21
+ ...(speech.voice !== undefined
22
+ ? {
23
+ voice: {
24
+ ...(speech.voice.gender !== undefined ? { gender: speech.voice.gender } : {}),
25
+ requirement: speech.voice.requirement ?? "preferred"
26
+ }
27
+ }
28
+ : {})
29
+ };
30
+ },
31
+ delivery: (request) => request.delivery.mode
32
+ };
33
+ const taskProfiles = new Map([
34
+ [textToText.kind, textToText],
35
+ [textToSpeech.kind, textToSpeech]
36
+ ]);
37
+ /**
38
+ * Adds a task profile. Internal and not exported from the package: a real task needs its
39
+ * schema kind and payload first (ADR 016). It exists so a test can prove a third task runs
40
+ * through routing, execution and output normalization without changing the engine.
41
+ * @internal
42
+ */
43
+ export function registerTaskProfile(profile) {
44
+ taskProfiles.set(profile.kind, profile);
45
+ }
46
+ /** The profile for `kind`, or `undefined` when this engine cannot execute the task. */
47
+ export function taskProfileFor(kind) {
48
+ return taskProfiles.get(kind);
49
+ }
50
+ /**
51
+ * Re-checks, immediately before an attempt, that the voice the plan bound for this
52
+ * provider still satisfies the request's privacy policy. The planner already chose the
53
+ * voice inside that policy; repeating the check here keeps privacy enforceable at the
54
+ * last point before data reaches the provider, independent of how the plan was produced.
55
+ * A violation fails the attempt before commit, so the run may fall back.
56
+ *
57
+ * @returns The exception to fail the attempt with, or `undefined` when the voice is allowed.
58
+ */
59
+ export function recheckBoundVoice(request, providerId, voice) {
60
+ if (voice?.networkRequired !== true || request.constraints?.privacy !== "local_required") {
61
+ return undefined;
62
+ }
63
+ return createCapabilityMismatch(`Provider '${providerId}' was bound to a voice that sends data off the device, which privacy=local_required forbids.`, { providerId, details: { code: "privacy_recheck_failed" } });
64
+ }
65
+ /**
66
+ * Checks a provider's output against the delivery the request asked for: inline text for
67
+ * `inline`, a playback completion for `playback`, a media artifact for `artifact` whose
68
+ * media type is one the request accepts. A provider never substitutes one for another
69
+ * (ADR 016 §4), so a mismatch is a provider fault, not a result.
70
+ *
71
+ * @returns The exception to fail the attempt with, or `undefined` when the output matches.
72
+ */
73
+ export function checkOutput(request, delivery, output, providerId) {
74
+ const expected = { inline: "text", playback: "playback", artifact: "artifact" }[delivery];
75
+ const matches = output?.type === expected &&
76
+ (expected === "text"
77
+ ? typeof output.text === "string"
78
+ : expected === "playback"
79
+ ? output.playback !== undefined
80
+ : output.artifact !== undefined && acceptsMediaType(request, output.artifact.mediaType));
81
+ if (matches)
82
+ return undefined;
83
+ return createInternal(`Provider '${providerId}' returned ${output === undefined ? "no output" : `a '${output.type}' output`} for a '${delivery}' request.`, { providerId, details: { code: "output_mismatch" } });
84
+ }
85
+ /** Whether `mediaType` (parameters ignored, case-insensitive) is one the request accepts. */
86
+ function acceptsMediaType(request, mediaType) {
87
+ const accepted = request.delivery?.mediaTypes;
88
+ if (accepted === undefined)
89
+ return true;
90
+ const base = (type) => type.split(";")[0].trim().toLowerCase();
91
+ return accepted.some((type) => base(type) === base(mediaType));
92
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/facade/text.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../../src/facade/text.ts"],"names":[],"mappings":"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"}
@@ -72,7 +72,9 @@ export function toTaskRequest(options) {
72
72
  export async function generateText(inderun, options) {
73
73
  const taskResult = await inderun.run(toTaskRequest(options));
74
74
  return {
75
- text: taskResult.output.text,
75
+ // A text run always produces text output; the generated type is optional only because
76
+ // TaskOutput also describes playback and artifacts (ADR 016).
77
+ text: taskResult.output.text ?? "",
76
78
  finishReason: taskResult.finishReason,
77
79
  ...(taskResult.usage !== undefined ? { usage: taskResult.usage } : {}),
78
80
  runId: taskResult.runId,
@@ -135,7 +137,7 @@ function settle(outcome) {
135
137
  switch (outcome.outcome) {
136
138
  case "completed":
137
139
  return {
138
- text: outcome.finalText ?? "",
140
+ text: outcome.finalText ?? outcome.output?.text ?? "",
139
141
  ...(outcome.finishReason !== undefined ? { finishReason: outcome.finishReason } : {}),
140
142
  ...(outcome.usage !== undefined ? { usage: outcome.usage } : {}),
141
143
  runId: outcome.runId,
@@ -144,7 +146,7 @@ function settle(outcome) {
144
146
  };
145
147
  case "cancelled":
146
148
  return {
147
- text: outcome.partialText ?? "",
149
+ text: outcome.partialText ?? outcome.output?.text ?? "",
148
150
  finishReason: "cancelled",
149
151
  runId: outcome.runId,
150
152
  ...provenance,
package/dist/index.d.ts CHANGED
@@ -36,7 +36,7 @@ export { AUTO_MODEL_PROFILE, type ModelProfiles } from "./core/model-profiles.js
36
36
  */
37
37
  export { IndeRun, type IndeRunOptions, type StreamHandleResult } from "./core/engine.js";
38
38
  export type { IndeRunApi } from "./core/generated/inderun-api.js";
39
- export type { ProviderCapabilitySnapshot, StreamRun } from "./core/provider.js";
39
+ export type { ArtifactChunk, ArtifactLimits, ProviderCapabilitySnapshot, StreamRun } from "./core/provider.js";
40
40
  /**
41
41
  * The contract types this SDK's own signatures take and return.
42
42
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;GAEG;AACH,OAAO,EACL,YAAY,EACZ,UAAU,EACV,aAAa,EACb,wBAAwB,EACxB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,UAAU,EAChB,MAAM,kBAAkB,CAAC;AAE1B;;;GAGG;AACH,OAAO,EACL,gBAAgB,EAChB,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EAC1B,MAAM,cAAc,CAAC;AAEtB;;;GAGG;AACH,OAAO,EAAE,kBAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAElF;;;;GAIG;AACH,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACzF,YAAY,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAClE,YAAY,EAAE,0BAA0B,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEhF;;;;;;;;;;;;;;GAcG;AACH,YAAY,EACV,mBAAmB,EACnB,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,eAAe,EACf,qBAAqB,EACrB,WAAW,EACX,UAAU,EACX,MAAM,8BAA8B,CAAC;AAEtC;;;;GAIG;AACH,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAExE;;;GAGG;AACH,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,oBAAoB,EACrB,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;GAEG;AACH,OAAO,EACL,YAAY,EACZ,UAAU,EACV,aAAa,EACb,wBAAwB,EACxB,KAAK,aAAa,EAClB,KAAK,YAAY,EACjB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,UAAU,EAChB,MAAM,kBAAkB,CAAC;AAE1B;;;GAGG;AACH,OAAO,EACL,gBAAgB,EAChB,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EAC1B,MAAM,cAAc,CAAC;AAEtB;;;GAGG;AACH,OAAO,EAAE,kBAAkB,EAAE,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAElF;;;;GAIG;AACH,OAAO,EAAE,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACzF,YAAY,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAC;AAClE,YAAY,EACV,aAAa,EACb,cAAc,EACd,0BAA0B,EAC1B,SAAS,EACV,MAAM,oBAAoB,CAAC;AAE5B;;;;;;;;;;;;;;GAcG;AACH,YAAY,EACV,mBAAmB,EACnB,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,eAAe,EACf,qBAAqB,EACrB,WAAW,EACX,UAAU,EACX,MAAM,8BAA8B,CAAC;AAEtC;;;;GAIG;AACH,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAExE;;;GAGG;AACH,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,oBAAoB,EACrB,MAAM,qBAAqB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@independo/inderun-web",
3
- "version": "0.5.0-rc.1",
3
+ "version": "0.5.0-rc.2",
4
4
  "description": "On-device AI with automatic cloud fallback for web apps. Runs ONNX Runtime or the Chrome Prompt API in the browser and falls back to an OpenAI-compatible endpoint, with streaming and cancellation behind one API.",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -72,8 +72,8 @@
72
72
  "README.md"
73
73
  ],
74
74
  "dependencies": {
75
- "@independo/inderun-route-core-wasm": "^0.5.0-rc.1",
76
- "@independo/inderun-contracts": "^0.5.0-rc.1"
75
+ "@independo/inderun-contracts": "^0.5.0-rc.2",
76
+ "@independo/inderun-route-core-wasm": "^0.5.0-rc.2"
77
77
  },
78
78
  "peerDependencies": {
79
79
  "@huggingface/transformers": "^4.0.0"
@@ -0,0 +1,280 @@
1
+ import type { ArtifactReadRequest, MediaArtifact } from "@independo/inderun-contracts";
2
+ import { createInternal, createUnavailable } from "./errors.js";
3
+
4
+ /**
5
+ * Size limits of an engine's artifact store (ADR 017). Every limit is in bytes.
6
+ */
7
+ export interface ArtifactLimits {
8
+ /** Largest single artifact a provider may write. Default 16 MiB. */
9
+ maxArtifactBytes?: number;
10
+ /** Most bytes the store holds across all undisposed artifacts. Default 64 MiB. */
11
+ maxStoreBytes?: number;
12
+ /** Most bytes one `readArtifact` call returns. Default 1 MiB. */
13
+ maxReadBytes?: number;
14
+ }
15
+
16
+ export const DEFAULT_ARTIFACT_LIMITS: Required<ArtifactLimits> = {
17
+ maxArtifactBytes: 16 * 1024 * 1024,
18
+ maxStoreBytes: 64 * 1024 * 1024,
19
+ maxReadBytes: 1024 * 1024
20
+ };
21
+
22
+ /**
23
+ * One range of an artifact's bytes, as `IndeRun.readArtifact` returns it.
24
+ */
25
+ export interface ArtifactChunk {
26
+ /** The artifact the bytes belong to. */
27
+ artifactId: string;
28
+ /** Byte offset of `bytes` within the artifact. */
29
+ offset: number;
30
+ /** The bytes of this range; empty when `offset` is the artifact's end. */
31
+ bytes: Uint8Array;
32
+ /** Size of the whole artifact in bytes. */
33
+ totalBytes: number;
34
+ /** Whether this range reaches the end of the artifact. */
35
+ done: boolean;
36
+ }
37
+
38
+ /** What a provider declares when it starts writing an artifact. */
39
+ export interface ArtifactSpec {
40
+ /** IANA media type of the bytes, e.g. `audio/wav`; parameters ride on it. */
41
+ mediaType: string;
42
+ /** Duration of time-based media in milliseconds, where the provider knows it. */
43
+ durationMs?: number;
44
+ }
45
+
46
+ /**
47
+ * Writes one artifact into the engine's store. `finish()` hands it over: it commits the run
48
+ * to the provider and returns the reference the provider puts in its output. A provider that
49
+ * fails or stops before `finish()` calls `abort()`; the engine also discards any writer an
50
+ * attempt left open.
51
+ */
52
+ export interface ArtifactWriter {
53
+ /**
54
+ * Appends bytes. Throws `Unavailable` (`details.code: "artifact_too_large"`) once the
55
+ * artifact would exceed the engine's per-artifact limit; the attempt has not committed, so
56
+ * the run may fall back.
57
+ */
58
+ write(bytes: Uint8Array): void;
59
+ /**
60
+ * Hands the finished artifact to the engine and commits the run. Throws `Unavailable`
61
+ * (`artifact_store_full`) when the store has no room, and `Unavailable`
62
+ * (`run_not_active`) when the run was cancelled or has ended -- in both cases before
63
+ * committing.
64
+ */
65
+ finish(): MediaArtifact;
66
+ /** Discards everything written. Idempotent. */
67
+ abort(): void;
68
+ }
69
+
70
+ /** Where a provider creates artifacts: `RunContext.artifacts`. */
71
+ export interface ArtifactSink {
72
+ create(spec: ArtifactSpec): ArtifactWriter;
73
+ }
74
+
75
+ interface StoredArtifact {
76
+ reference: MediaArtifact;
77
+ bytes: Uint8Array;
78
+ }
79
+
80
+ /**
81
+ * The engine-owned, in-memory artifact store (ADR 017): one per `IndeRun` instance. It holds
82
+ * every finished artifact until the application disposes it; there are no temporary files,
83
+ * so nothing outlives the process. Reads are ranged and bounded by `maxReadBytes`.
84
+ */
85
+ export class ArtifactStore {
86
+ private readonly limits: Required<ArtifactLimits>;
87
+ private readonly artifacts = new Map<string, StoredArtifact>();
88
+ private storedBytes = 0;
89
+
90
+ constructor(limits: ArtifactLimits = {}) {
91
+ this.limits = { ...DEFAULT_ARTIFACT_LIMITS, ...limits };
92
+ }
93
+
94
+ /**
95
+ * Opens the artifacts of one attempt. `commit` is the attempt's commit hook; a writer's
96
+ * `finish()` calls it, and refuses the handoff when it returns `false`.
97
+ */
98
+ attempt(commit: () => boolean): AttemptArtifacts {
99
+ return new AttemptArtifacts(this, commit);
100
+ }
101
+
102
+ /** Reads one range. See `IndeRunApi.readArtifact` for the contract. */
103
+ read(request: ArtifactReadRequest): ArtifactChunk {
104
+ const stored = this.artifacts.get(request.artifactId);
105
+ if (stored === undefined) {
106
+ throw createUnavailable(`Artifact '${request.artifactId}' is not held by this engine.`, {
107
+ retryable: false,
108
+ details: { code: "artifact_not_found" }
109
+ });
110
+ }
111
+ const totalBytes = stored.bytes.byteLength;
112
+ const offset = request.offset ?? 0;
113
+ if (offset > totalBytes) {
114
+ const validationIssues = [
115
+ {
116
+ path: "/offset",
117
+ message: `offset ${offset} is beyond the artifact's ${totalBytes} bytes`
118
+ }
119
+ ];
120
+ throw createInternal(
121
+ `Validation failed for ArtifactReadRequest: /offset - ${validationIssues[0]!.message}`,
122
+ { details: { validationIssues } }
123
+ );
124
+ }
125
+ const length = Math.min(request.maxBytes ?? this.limits.maxReadBytes, this.limits.maxReadBytes);
126
+ const end = Math.min(offset + length, totalBytes);
127
+ return {
128
+ artifactId: request.artifactId,
129
+ offset,
130
+ // A copy: the caller may keep or mutate it without touching the stored bytes.
131
+ bytes: stored.bytes.slice(offset, end),
132
+ totalBytes,
133
+ done: end === totalBytes
134
+ };
135
+ }
136
+
137
+ /** Releases an artifact's bytes. Idempotent; unknown ids are ignored. */
138
+ dispose(artifactId: string): void {
139
+ const stored = this.artifacts.get(artifactId);
140
+ if (stored === undefined) return;
141
+ this.artifacts.delete(artifactId);
142
+ this.storedBytes -= stored.bytes.byteLength;
143
+ }
144
+
145
+ /** The stored reference for `artifactId`, or `undefined` once it was disposed. */
146
+ reference(artifactId: string): MediaArtifact | undefined {
147
+ return this.artifacts.get(artifactId)?.reference;
148
+ }
149
+
150
+ /** @internal Registers a finished artifact. */
151
+ store(spec: ArtifactSpec, bytes: Uint8Array): MediaArtifact {
152
+ if (this.storedBytes + bytes.byteLength > this.limits.maxStoreBytes) {
153
+ throw createUnavailable(
154
+ "The engine's artifact store is full; dispose artifacts to free it.",
155
+ {
156
+ details: { code: "artifact_store_full" }
157
+ }
158
+ );
159
+ }
160
+ const reference: MediaArtifact = {
161
+ id: `art_${randomId()}`,
162
+ mediaType: spec.mediaType,
163
+ owner: "engine",
164
+ lifetime: "until_disposed",
165
+ byteLength: bytes.byteLength,
166
+ ...(spec.durationMs !== undefined ? { durationMs: spec.durationMs } : {})
167
+ };
168
+ this.artifacts.set(reference.id, { reference, bytes });
169
+ this.storedBytes += bytes.byteLength;
170
+ return reference;
171
+ }
172
+
173
+ /** @internal The whole artifact, for writing it to an application destination. */
174
+ bytes(artifactId: string): Uint8Array | undefined {
175
+ return this.artifacts.get(artifactId)?.bytes;
176
+ }
177
+
178
+ /** @internal */
179
+ get maxArtifactBytes(): number {
180
+ return this.limits.maxArtifactBytes;
181
+ }
182
+ }
183
+
184
+ /**
185
+ * The artifacts one attempt created. The engine keeps the one the attempt returned and
186
+ * releases the rest: everything from an attempt that failed, was cancelled or lost to
187
+ * fallback, and anything a winning attempt finished but did not return. A returned artifact
188
+ * is never released behind the application's back.
189
+ */
190
+ export class AttemptArtifacts implements ArtifactSink {
191
+ private readonly finished = new Set<string>();
192
+ private readonly open = new Set<ArtifactWriter>();
193
+ private released = false;
194
+
195
+ constructor(
196
+ private readonly store: ArtifactStore,
197
+ private readonly commit: () => boolean
198
+ ) {}
199
+
200
+ create(spec: ArtifactSpec): ArtifactWriter {
201
+ const chunks: Uint8Array[] = [];
202
+ let size = 0;
203
+ let closed = false;
204
+ const writer: ArtifactWriter = {
205
+ write: (bytes) => {
206
+ if (closed) throw createInternal("The artifact writer is already closed.");
207
+ if (size + bytes.byteLength > this.store.maxArtifactBytes) {
208
+ writer.abort();
209
+ throw createUnavailable(
210
+ `The artifact exceeds the engine's ${this.store.maxArtifactBytes}-byte limit.`,
211
+ { details: { code: "artifact_too_large" } }
212
+ );
213
+ }
214
+ chunks.push(bytes.slice());
215
+ size += bytes.byteLength;
216
+ },
217
+ finish: () => {
218
+ if (closed) throw createInternal("The artifact writer is already closed.");
219
+ const bytes = concat(chunks, size);
220
+ writer.abort();
221
+ // A provider that keeps working after its attempt ended (a late callback, say) must
222
+ // not leave an artifact behind that nobody owns.
223
+ if (this.released) {
224
+ throw createUnavailable("The attempt has ended; the artifact was discarded.", {
225
+ details: { code: "run_not_active" }
226
+ });
227
+ }
228
+ // Stored before the commit so a full store fails the attempt pre-commit; dropped
229
+ // again when the run refuses the commit.
230
+ const reference = this.store.store(spec, bytes);
231
+ if (!this.commit()) {
232
+ this.store.dispose(reference.id);
233
+ throw createUnavailable("The run is no longer active; the artifact was discarded.", {
234
+ details: { code: "run_not_active" }
235
+ });
236
+ }
237
+ this.finished.add(reference.id);
238
+ return reference;
239
+ },
240
+ abort: () => {
241
+ closed = true;
242
+ chunks.length = 0;
243
+ this.open.delete(writer);
244
+ }
245
+ };
246
+ this.open.add(writer);
247
+ return writer;
248
+ }
249
+
250
+ /** Whether `artifactId` was finished by this attempt and is still held. */
251
+ owns(artifactId: string): boolean {
252
+ return this.finished.has(artifactId) && this.store.reference(artifactId) !== undefined;
253
+ }
254
+
255
+ /** Releases every artifact of this attempt except `keep`, and closes open writers. */
256
+ release(keep?: string): void {
257
+ this.released = true;
258
+ for (const writer of [...this.open]) writer.abort();
259
+ for (const id of this.finished) {
260
+ if (id !== keep) this.store.dispose(id);
261
+ }
262
+ this.finished.clear();
263
+ }
264
+ }
265
+
266
+ function concat(chunks: Uint8Array[], size: number): Uint8Array {
267
+ const bytes = new Uint8Array(size);
268
+ let offset = 0;
269
+ for (const chunk of chunks) {
270
+ bytes.set(chunk, offset);
271
+ offset += chunk.byteLength;
272
+ }
273
+ return bytes;
274
+ }
275
+
276
+ function randomId(): string {
277
+ const random = new Uint8Array(12);
278
+ globalThis.crypto.getRandomValues(random);
279
+ return Array.from(random, (byte) => byte.toString(16).padStart(2, "0")).join("");
280
+ }