@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.
- 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 +44 -1
- package/dist/core/provider.d.ts.map +1 -1
- package/dist/core/route-planner.d.ts.map +1 -1
- package/dist/core/route-planner.js +5 -6
- 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/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 +53 -0
- package/src/core/route-planner.ts +5 -6
- 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
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
TaskRequest
|
|
5
5
|
} from "@independo/inderun-contracts";
|
|
6
6
|
import type { HostServices } from "./host.js";
|
|
7
|
+
import { taskProfileFor } from "./task-profiles.js";
|
|
7
8
|
import type {
|
|
8
9
|
ProviderAdapter,
|
|
9
10
|
ProviderDescriptor,
|
|
@@ -204,14 +205,12 @@ export function buildSharedPlannerInput(
|
|
|
204
205
|
}
|
|
205
206
|
|
|
206
207
|
/**
|
|
207
|
-
* Projects the request's task onto the requirements the planner can check
|
|
208
|
-
*
|
|
209
|
-
*
|
|
208
|
+
* Projects the request's task onto the requirements the planner can check, through the
|
|
209
|
+
* kind's task profile. A kind without a profile carries the kind alone, which the planner
|
|
210
|
+
* matches against declarations and nothing else.
|
|
210
211
|
*/
|
|
211
212
|
function plannerTask(request: TaskRequest): SharedPlannerInput["task"] {
|
|
212
|
-
return request.task.kind
|
|
213
|
-
? { kind: request.task.kind, inputs: ["text"] }
|
|
214
|
-
: { kind: request.task.kind };
|
|
213
|
+
return taskProfileFor(request.task.kind)?.plannerTask(request) ?? { kind: request.task.kind };
|
|
215
214
|
}
|
|
216
215
|
|
|
217
216
|
/**
|
package/src/core/router.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { TaskRequest } from "@independo/inderun-contracts";
|
|
|
2
2
|
import type { HostServices } from "./host.js";
|
|
3
3
|
import type { ProviderAdapter } from "./provider.js";
|
|
4
4
|
import type { ProviderRegistry } from "./registry.js";
|
|
5
|
+
import type { BoundVoice } from "./task-profiles.js";
|
|
5
6
|
import {
|
|
6
7
|
createCapabilityMismatch,
|
|
7
8
|
createInternal,
|
|
@@ -19,6 +20,18 @@ import {
|
|
|
19
20
|
WasmRoutePlanner
|
|
20
21
|
} from "./route-planner.js";
|
|
21
22
|
|
|
23
|
+
/**
|
|
24
|
+
* One provider in the planned chain, with the voice the plan bound it to (speech only).
|
|
25
|
+
*/
|
|
26
|
+
export interface RouteCandidate {
|
|
27
|
+
provider: ProviderAdapter;
|
|
28
|
+
/**
|
|
29
|
+
* The voice this provider must use, chosen by the planner within the request's privacy
|
|
30
|
+
* policy. Absent when the task has no voice or the provider does not list its voices.
|
|
31
|
+
*/
|
|
32
|
+
voice?: BoundVoice;
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
/**
|
|
23
36
|
* Output structure from the Router containing the chosen provider and a route selection description.
|
|
24
37
|
*/
|
|
@@ -31,6 +44,10 @@ export interface RouteSelection {
|
|
|
31
44
|
* Ordered fallback providers to try if the primary provider fails before producing a final result.
|
|
32
45
|
*/
|
|
33
46
|
fallbackProviders: ProviderAdapter[];
|
|
47
|
+
/**
|
|
48
|
+
* The whole chain in attempt order (`provider` first), each with its bound voice.
|
|
49
|
+
*/
|
|
50
|
+
candidates: RouteCandidate[];
|
|
34
51
|
/**
|
|
35
52
|
* The shared route plan used to select this provider chain.
|
|
36
53
|
*/
|
|
@@ -113,20 +130,26 @@ export class Router {
|
|
|
113
130
|
snapshots: ProviderRuntimeSnapshot[],
|
|
114
131
|
routePlan: SharedPlannerRoutePlan
|
|
115
132
|
): RouteSelection {
|
|
116
|
-
const
|
|
117
|
-
.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
133
|
+
const candidates = routePlan.candidates.flatMap((candidate): RouteCandidate[] => {
|
|
134
|
+
const snapshot = snapshots.find((s) => s.descriptor.id === candidate.providerId);
|
|
135
|
+
if (snapshot === undefined) return [];
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
provider: snapshot.provider,
|
|
139
|
+
...(candidate.voice !== undefined ? { voice: candidate.voice } : {})
|
|
140
|
+
}
|
|
141
|
+
];
|
|
142
|
+
});
|
|
121
143
|
|
|
122
|
-
const selected =
|
|
144
|
+
const selected = candidates[0];
|
|
123
145
|
if (!selected) {
|
|
124
146
|
throw createInternal("Route plan selected a provider that is no longer registered.");
|
|
125
147
|
}
|
|
126
148
|
|
|
127
149
|
return {
|
|
128
150
|
provider: selected.provider,
|
|
129
|
-
fallbackProviders:
|
|
151
|
+
fallbackProviders: candidates.slice(1).map((candidate) => candidate.provider),
|
|
152
|
+
candidates,
|
|
130
153
|
routePlan,
|
|
131
154
|
explanation: routePlan.explanation.summary
|
|
132
155
|
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SharedRoutePlan,
|
|
3
|
+
SharedRoutePlannerInput,
|
|
4
|
+
TaskOutput,
|
|
5
|
+
TaskRequest
|
|
6
|
+
} from "@independo/inderun-contracts";
|
|
7
|
+
import { createCapabilityMismatch, createInternal, type IndeRunException } from "./errors.js";
|
|
8
|
+
|
|
9
|
+
/** How a task's result reaches the application: `inline`, `playback` or `artifact`. */
|
|
10
|
+
export type OutputDelivery = NonNullable<TaskRequest["delivery"]>["mode"];
|
|
11
|
+
/** The provider-neutral voice a route candidate is bound to. */
|
|
12
|
+
export type BoundVoice = NonNullable<SharedRoutePlan["candidates"][number]["voice"]>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What the engine needs to know about one task kind (ADR 016). This table is the only
|
|
16
|
+
* place the engine branches on `task.kind`: which kinds it executes, the projection onto
|
|
17
|
+
* the planner's task requirements, and the output delivery a result must have. (Per-kind
|
|
18
|
+
* request requirements are the schema's `if`/`then` branches, checked by the validator.) Everything else — routing, fallback, cancellation, provenance — is task-agnostic,
|
|
19
|
+
* so a new task adds an entry here rather than a second execution path.
|
|
20
|
+
*
|
|
21
|
+
* The Swift (`TaskProfiles.swift`) and Kotlin (`TaskProfiles.kt`) engines keep the same
|
|
22
|
+
* table; `contracts/fixtures/task-contracts/` holds the planner projection all three must
|
|
23
|
+
* produce for each request.
|
|
24
|
+
*/
|
|
25
|
+
export interface TaskProfile {
|
|
26
|
+
/** The `task.kind` this profile describes. */
|
|
27
|
+
readonly kind: string;
|
|
28
|
+
/** Projects the request onto the requirements the shared planner can check. */
|
|
29
|
+
plannerTask(request: TaskRequest): SharedRoutePlannerInput["task"];
|
|
30
|
+
/** The output delivery the request asks for: `inline` for text, else `delivery.mode`. */
|
|
31
|
+
delivery(request: TaskRequest): OutputDelivery;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const textToText: TaskProfile = {
|
|
35
|
+
kind: "text_to_text",
|
|
36
|
+
plannerTask: () => ({ kind: "text_to_text", inputs: ["text"] }),
|
|
37
|
+
delivery: () => "inline"
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const textToSpeech: TaskProfile = {
|
|
41
|
+
kind: "text_to_speech",
|
|
42
|
+
plannerTask: (request) => {
|
|
43
|
+
// The schema requires both for this kind; validation runs before projection.
|
|
44
|
+
const speech = request.speech!;
|
|
45
|
+
const delivery = request.delivery!;
|
|
46
|
+
return {
|
|
47
|
+
kind: "text_to_speech",
|
|
48
|
+
inputs: ["text"],
|
|
49
|
+
delivery: {
|
|
50
|
+
mode: delivery.mode,
|
|
51
|
+
...(delivery.mediaTypes !== undefined ? { mediaTypes: [...delivery.mediaTypes] } : {})
|
|
52
|
+
},
|
|
53
|
+
locale: { tag: speech.locale, match: speech.localeMatch ?? "language" },
|
|
54
|
+
...(speech.voice !== undefined
|
|
55
|
+
? {
|
|
56
|
+
voice: {
|
|
57
|
+
...(speech.voice.gender !== undefined ? { gender: speech.voice.gender } : {}),
|
|
58
|
+
requirement: speech.voice.requirement ?? "preferred"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
: {})
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
delivery: (request) => request.delivery!.mode
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const taskProfiles = new Map<string, TaskProfile>([
|
|
68
|
+
[textToText.kind, textToText],
|
|
69
|
+
[textToSpeech.kind, textToSpeech]
|
|
70
|
+
]);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Adds a task profile. Internal and not exported from the package: a real task needs its
|
|
74
|
+
* schema kind and payload first (ADR 016). It exists so a test can prove a third task runs
|
|
75
|
+
* through routing, execution and output normalization without changing the engine.
|
|
76
|
+
* @internal
|
|
77
|
+
*/
|
|
78
|
+
export function registerTaskProfile(profile: TaskProfile): void {
|
|
79
|
+
taskProfiles.set(profile.kind, profile);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The profile for `kind`, or `undefined` when this engine cannot execute the task. */
|
|
83
|
+
export function taskProfileFor(kind: string): TaskProfile | undefined {
|
|
84
|
+
return taskProfiles.get(kind);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Re-checks, immediately before an attempt, that the voice the plan bound for this
|
|
89
|
+
* provider still satisfies the request's privacy policy. The planner already chose the
|
|
90
|
+
* voice inside that policy; repeating the check here keeps privacy enforceable at the
|
|
91
|
+
* last point before data reaches the provider, independent of how the plan was produced.
|
|
92
|
+
* A violation fails the attempt before commit, so the run may fall back.
|
|
93
|
+
*
|
|
94
|
+
* @returns The exception to fail the attempt with, or `undefined` when the voice is allowed.
|
|
95
|
+
*/
|
|
96
|
+
export function recheckBoundVoice(
|
|
97
|
+
request: TaskRequest,
|
|
98
|
+
providerId: string,
|
|
99
|
+
voice: BoundVoice | undefined
|
|
100
|
+
): IndeRunException | undefined {
|
|
101
|
+
if (voice?.networkRequired !== true || request.constraints?.privacy !== "local_required") {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
return createCapabilityMismatch(
|
|
105
|
+
`Provider '${providerId}' was bound to a voice that sends data off the device, which privacy=local_required forbids.`,
|
|
106
|
+
{ providerId, details: { code: "privacy_recheck_failed" } }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Checks a provider's output against the delivery the request asked for: inline text for
|
|
112
|
+
* `inline`, a playback completion for `playback`, a media artifact for `artifact` whose
|
|
113
|
+
* media type is one the request accepts. A provider never substitutes one for another
|
|
114
|
+
* (ADR 016 §4), so a mismatch is a provider fault, not a result.
|
|
115
|
+
*
|
|
116
|
+
* @returns The exception to fail the attempt with, or `undefined` when the output matches.
|
|
117
|
+
*/
|
|
118
|
+
export function checkOutput(
|
|
119
|
+
request: TaskRequest,
|
|
120
|
+
delivery: OutputDelivery,
|
|
121
|
+
output: TaskOutput | undefined,
|
|
122
|
+
providerId: string
|
|
123
|
+
): IndeRunException | undefined {
|
|
124
|
+
const expected = { inline: "text", playback: "playback", artifact: "artifact" }[delivery];
|
|
125
|
+
const matches =
|
|
126
|
+
output?.type === expected &&
|
|
127
|
+
(expected === "text"
|
|
128
|
+
? typeof output.text === "string"
|
|
129
|
+
: expected === "playback"
|
|
130
|
+
? output.playback !== undefined
|
|
131
|
+
: output.artifact !== undefined && acceptsMediaType(request, output.artifact.mediaType));
|
|
132
|
+
if (matches) return undefined;
|
|
133
|
+
return createInternal(
|
|
134
|
+
`Provider '${providerId}' returned ${output === undefined ? "no output" : `a '${output.type}' output`} for a '${delivery}' request.`,
|
|
135
|
+
{ providerId, details: { code: "output_mismatch" } }
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Whether `mediaType` (parameters ignored, case-insensitive) is one the request accepts. */
|
|
140
|
+
function acceptsMediaType(request: TaskRequest, mediaType: string): boolean {
|
|
141
|
+
const accepted = request.delivery?.mediaTypes;
|
|
142
|
+
if (accepted === undefined) return true;
|
|
143
|
+
const base = (type: string) => type.split(";")[0]!.trim().toLowerCase();
|
|
144
|
+
return accepted.some((type) => base(type) === base(mediaType));
|
|
145
|
+
}
|
package/src/facade/text.ts
CHANGED
|
@@ -176,7 +176,9 @@ export async function generateText(
|
|
|
176
176
|
): Promise<GeneratedText> {
|
|
177
177
|
const taskResult = await inderun.run(toTaskRequest(options));
|
|
178
178
|
return {
|
|
179
|
-
text
|
|
179
|
+
// A text run always produces text output; the generated type is optional only because
|
|
180
|
+
// TaskOutput also describes playback and artifacts (ADR 016).
|
|
181
|
+
text: taskResult.output.text ?? "",
|
|
180
182
|
finishReason: taskResult.finishReason,
|
|
181
183
|
...(taskResult.usage !== undefined ? { usage: taskResult.usage } : {}),
|
|
182
184
|
runId: taskResult.runId,
|
|
@@ -245,7 +247,7 @@ function settle(outcome: StreamTerminalOutcome): StreamedText {
|
|
|
245
247
|
switch (outcome.outcome) {
|
|
246
248
|
case "completed":
|
|
247
249
|
return {
|
|
248
|
-
text: outcome.finalText ?? "",
|
|
250
|
+
text: outcome.finalText ?? outcome.output?.text ?? "",
|
|
249
251
|
...(outcome.finishReason !== undefined ? { finishReason: outcome.finishReason } : {}),
|
|
250
252
|
...(outcome.usage !== undefined ? { usage: outcome.usage } : {}),
|
|
251
253
|
runId: outcome.runId,
|
|
@@ -254,7 +256,7 @@ function settle(outcome: StreamTerminalOutcome): StreamedText {
|
|
|
254
256
|
};
|
|
255
257
|
case "cancelled":
|
|
256
258
|
return {
|
|
257
|
-
text: outcome.partialText ?? "",
|
|
259
|
+
text: outcome.partialText ?? outcome.output?.text ?? "",
|
|
258
260
|
finishReason: "cancelled",
|
|
259
261
|
runId: outcome.runId,
|
|
260
262
|
...provenance,
|
package/src/index.ts
CHANGED
|
@@ -54,7 +54,12 @@ export { AUTO_MODEL_PROFILE, type ModelProfiles } from "./core/model-profiles.js
|
|
|
54
54
|
*/
|
|
55
55
|
export { IndeRun, type IndeRunOptions, type StreamHandleResult } from "./core/engine.js";
|
|
56
56
|
export type { IndeRunApi } from "./core/generated/inderun-api.js";
|
|
57
|
-
export type {
|
|
57
|
+
export type {
|
|
58
|
+
ArtifactChunk,
|
|
59
|
+
ArtifactLimits,
|
|
60
|
+
ProviderCapabilitySnapshot,
|
|
61
|
+
StreamRun
|
|
62
|
+
} from "./core/provider.js";
|
|
58
63
|
|
|
59
64
|
/**
|
|
60
65
|
* The contract types this SDK's own signatures take and return.
|