@opengeni/core 2.8.0-canary.1 → 2.8.2-canary.1
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/dependencies.d.ts +4 -2
- package/dist/domain/github-skill-source.d.ts +5 -0
- package/dist/domain/packs.d.ts +2 -3
- package/dist/domain/pr-review.d.ts +2 -1
- package/dist/domain/remember.d.ts +2 -2
- package/dist/domain/sessions.d.ts +1 -0
- package/dist/domain/skill-search.d.ts +30 -0
- package/dist/domain/skills.d.ts +73 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +522 -30
- package/dist/index.js.map +1 -1
- package/dist/transcription.d.ts +12 -1
- package/package.json +12 -12
- package/src/dependencies.ts +4 -2
- package/src/domain/github-skill-source.ts +142 -0
- package/src/domain/packs.ts +16 -8
- package/src/domain/product-integration-pack.ts +1 -0
- package/src/domain/remember.ts +7 -0
- package/src/domain/scheduled-tasks.ts +28 -0
- package/src/domain/sessions.ts +17 -0
- package/src/domain/skill-imports.ts +99 -15
- package/src/domain/skill-search.ts +218 -0
- package/src/domain/skills.ts +87 -0
- package/src/index.ts +3 -0
- package/src/sandbox/routing.ts +40 -1
- package/src/sandbox/runtime-settings.ts +9 -2
- package/src/transcription.ts +13 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type { Settings } from "@opengeni/config";
|
|
2
|
+
import { pinnedFetch, readResponseJsonBounded } from "@opengeni/network";
|
|
3
|
+
|
|
4
|
+
export type PublicSkillSearchInput = Readonly<{
|
|
5
|
+
query: string;
|
|
6
|
+
limit?: number;
|
|
7
|
+
owner?: string;
|
|
8
|
+
}>;
|
|
9
|
+
|
|
10
|
+
export type PublicSkillSearchResult = Readonly<{
|
|
11
|
+
provider: "skills_sh";
|
|
12
|
+
query: string;
|
|
13
|
+
items: readonly Readonly<{
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
source: string;
|
|
17
|
+
skillId: string;
|
|
18
|
+
url: string;
|
|
19
|
+
installs: number;
|
|
20
|
+
}>[];
|
|
21
|
+
nextCursor: null;
|
|
22
|
+
}>;
|
|
23
|
+
|
|
24
|
+
export type PublicSkillSearchClient = Readonly<{
|
|
25
|
+
search(input: PublicSkillSearchInput): Promise<PublicSkillSearchResult>;
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
export type PublicSkillSearchErrorCode =
|
|
29
|
+
| "invalid_query"
|
|
30
|
+
| "rate_limited"
|
|
31
|
+
| "timeout"
|
|
32
|
+
| "unavailable"
|
|
33
|
+
| "invalid_response";
|
|
34
|
+
|
|
35
|
+
export class PublicSkillSearchError extends Error {
|
|
36
|
+
constructor(
|
|
37
|
+
readonly code: PublicSkillSearchErrorCode,
|
|
38
|
+
message: string,
|
|
39
|
+
readonly retryAfterSeconds: number | null = null,
|
|
40
|
+
) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "PublicSkillSearchError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Undocumented, unauthenticated compatibility endpoint used by the official CLI.
|
|
47
|
+
// Verified 2026-09-08 against vercel-labs/skills commit
|
|
48
|
+
// 1682051d48c34f5eb135e6475c1a965dce05e820, src/find.ts. This is NOT the documented
|
|
49
|
+
// /api/v1/skills/search API (which requires Vercel OIDC). Keep this contract behind
|
|
50
|
+
// this adapter: no promised pagination/quota, no CLI dependency or credentials.
|
|
51
|
+
const endpoint = "https://skills.sh/api/search";
|
|
52
|
+
const timeoutMs = 10_000;
|
|
53
|
+
const maxResponseBytes = 256 * 1024;
|
|
54
|
+
const maxResults = 20;
|
|
55
|
+
const ownerPattern = /^[a-z0-9](?:[a-z0-9-]{0,38})$/iu;
|
|
56
|
+
const segmentPattern = /^[a-z0-9](?:[a-z0-9._-]{0,98}[a-z0-9])?$/iu;
|
|
57
|
+
|
|
58
|
+
export function createPublicSkillSearchClient(
|
|
59
|
+
settings: Settings,
|
|
60
|
+
fetcher: typeof pinnedFetch = pinnedFetch,
|
|
61
|
+
): PublicSkillSearchClient {
|
|
62
|
+
return {
|
|
63
|
+
async search(input) {
|
|
64
|
+
const query = input.query.trim();
|
|
65
|
+
const limit = input.limit ?? maxResults;
|
|
66
|
+
const owner = input.owner?.trim().toLowerCase();
|
|
67
|
+
if (
|
|
68
|
+
query.length < 2 ||
|
|
69
|
+
query.length > 200 ||
|
|
70
|
+
/[\u0000-\u001f\u007f]/u.test(query) ||
|
|
71
|
+
!Number.isSafeInteger(limit) ||
|
|
72
|
+
limit < 1 ||
|
|
73
|
+
limit > maxResults ||
|
|
74
|
+
(owner !== undefined && !ownerPattern.test(owner))
|
|
75
|
+
) {
|
|
76
|
+
throw new PublicSkillSearchError("invalid_query", "Invalid public Skill search parameters");
|
|
77
|
+
}
|
|
78
|
+
const url = new URL(endpoint);
|
|
79
|
+
url.searchParams.set("q", query);
|
|
80
|
+
url.searchParams.set("limit", String(limit));
|
|
81
|
+
if (owner) url.searchParams.set("owner", owner);
|
|
82
|
+
const controller = new AbortController();
|
|
83
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
84
|
+
const deadline = new Promise<never>((_, reject) => {
|
|
85
|
+
timer = setTimeout(() => {
|
|
86
|
+
controller.abort();
|
|
87
|
+
reject(new PublicSkillSearchError("timeout", "Public Skill search timed out"));
|
|
88
|
+
}, timeoutMs);
|
|
89
|
+
});
|
|
90
|
+
try {
|
|
91
|
+
return await Promise.race([
|
|
92
|
+
deadline,
|
|
93
|
+
(async () => {
|
|
94
|
+
const response = await fetcher(
|
|
95
|
+
url,
|
|
96
|
+
{
|
|
97
|
+
method: "GET",
|
|
98
|
+
headers: { accept: "application/json" },
|
|
99
|
+
credentials: "omit",
|
|
100
|
+
redirect: "manual",
|
|
101
|
+
signal: controller.signal,
|
|
102
|
+
},
|
|
103
|
+
settings,
|
|
104
|
+
{ label: "Public Skill search", requireHttpsOutsideLocalTest: true },
|
|
105
|
+
);
|
|
106
|
+
if (controller.signal.aborted || !response.ok) {
|
|
107
|
+
void response.body?.cancel().catch(() => undefined);
|
|
108
|
+
if (controller.signal.aborted) {
|
|
109
|
+
throw new PublicSkillSearchError("timeout", "Public Skill search timed out");
|
|
110
|
+
}
|
|
111
|
+
if (response.status === 429) {
|
|
112
|
+
throw new PublicSkillSearchError(
|
|
113
|
+
"rate_limited",
|
|
114
|
+
"Public Skill search is rate limited",
|
|
115
|
+
parseRetryAfter(response.headers.get("retry-after")),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
throw new PublicSkillSearchError("unavailable", "Public Skill search is unavailable");
|
|
119
|
+
}
|
|
120
|
+
let payload: unknown;
|
|
121
|
+
try {
|
|
122
|
+
payload = await readResponseJsonBounded(
|
|
123
|
+
response,
|
|
124
|
+
maxResponseBytes,
|
|
125
|
+
"Public Skill search",
|
|
126
|
+
{
|
|
127
|
+
signal: controller.signal,
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
} catch {
|
|
131
|
+
throw new PublicSkillSearchError(
|
|
132
|
+
controller.signal.aborted ? "timeout" : "invalid_response",
|
|
133
|
+
controller.signal.aborted
|
|
134
|
+
? "Public Skill search timed out"
|
|
135
|
+
: "Public Skill search returned an invalid response",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return normalizeResponse(payload, query, limit, owner);
|
|
139
|
+
})(),
|
|
140
|
+
]);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error instanceof PublicSkillSearchError) throw error;
|
|
143
|
+
throw new PublicSkillSearchError(
|
|
144
|
+
controller.signal.aborted ? "timeout" : "unavailable",
|
|
145
|
+
controller.signal.aborted
|
|
146
|
+
? "Public Skill search timed out"
|
|
147
|
+
: "Public Skill search is unavailable",
|
|
148
|
+
);
|
|
149
|
+
} finally {
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function normalizeResponse(
|
|
157
|
+
payload: unknown,
|
|
158
|
+
query: string,
|
|
159
|
+
limit: number,
|
|
160
|
+
owner: string | undefined,
|
|
161
|
+
): PublicSkillSearchResult {
|
|
162
|
+
const invalid = () =>
|
|
163
|
+
new PublicSkillSearchError(
|
|
164
|
+
"invalid_response",
|
|
165
|
+
"Public Skill search returned an invalid response",
|
|
166
|
+
);
|
|
167
|
+
if (!isRecord(payload) || !Array.isArray(payload.skills) || payload.skills.length > maxResults) {
|
|
168
|
+
throw invalid();
|
|
169
|
+
}
|
|
170
|
+
const seen = new Set<string>();
|
|
171
|
+
const items: PublicSkillSearchResult["items"][number][] = [];
|
|
172
|
+
for (const skill of payload.skills) {
|
|
173
|
+
if (!isRecord(skill) || typeof skill.id !== "string" || typeof skill.source !== "string")
|
|
174
|
+
throw invalid();
|
|
175
|
+
const parts = skill.id.split("/");
|
|
176
|
+
if (
|
|
177
|
+
parts.length !== 3 ||
|
|
178
|
+
!ownerPattern.test(parts[0]!) ||
|
|
179
|
+
!parts.every((part) => segmentPattern.test(part)) ||
|
|
180
|
+
skill.source !== parts.slice(0, 2).join("/") ||
|
|
181
|
+
(skill.skillId !== undefined && skill.skillId !== parts[2]) ||
|
|
182
|
+
typeof skill.name !== "string" ||
|
|
183
|
+
!skill.name.trim() ||
|
|
184
|
+
skill.name.length > 200 ||
|
|
185
|
+
/[\u0000-\u001f\u007f]/u.test(skill.name) ||
|
|
186
|
+
typeof skill.installs !== "number" ||
|
|
187
|
+
!Number.isSafeInteger(skill.installs) ||
|
|
188
|
+
skill.installs < 0 ||
|
|
189
|
+
(owner !== undefined && parts[0]!.toLowerCase() !== owner)
|
|
190
|
+
)
|
|
191
|
+
throw invalid();
|
|
192
|
+
if (seen.has(skill.id)) continue;
|
|
193
|
+
seen.add(skill.id);
|
|
194
|
+
items.push({
|
|
195
|
+
id: skill.id,
|
|
196
|
+
name: skill.name.trim(),
|
|
197
|
+
source: skill.source,
|
|
198
|
+
skillId: parts[2]!,
|
|
199
|
+
url: `https://skills.sh/${parts.map(encodeURIComponent).join("/")}`,
|
|
200
|
+
installs: skill.installs,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return { provider: "skills_sh", query, items: items.slice(0, limit), nextCursor: null };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
207
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function parseRetryAfter(value: string | null): number | null {
|
|
211
|
+
if (value === null) return null;
|
|
212
|
+
if (/^\d+$/u.test(value)) {
|
|
213
|
+
const seconds = Number(value);
|
|
214
|
+
return Number.isSafeInteger(seconds) ? seconds : null;
|
|
215
|
+
}
|
|
216
|
+
const date = Date.parse(value);
|
|
217
|
+
return Number.isFinite(date) ? Math.max(0, Math.ceil((date - Date.now()) / 1000)) : null;
|
|
218
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SkillFile,
|
|
3
|
+
SkillInstallInput,
|
|
4
|
+
SkillRevisionInput,
|
|
5
|
+
SkillSaveInput,
|
|
6
|
+
} from "@opengeni/contracts";
|
|
7
|
+
import { validateSkillTextFiles } from "@opengeni/contracts";
|
|
8
|
+
import { buildPortableSkillArtifact } from "@opengeni/runtime/skill-library";
|
|
9
|
+
import {
|
|
10
|
+
applySkillLifecycle,
|
|
11
|
+
listSkillRecords,
|
|
12
|
+
skillFilesContentHash,
|
|
13
|
+
type Database,
|
|
14
|
+
type SkillReadContext,
|
|
15
|
+
} from "@opengeni/db";
|
|
16
|
+
export { replayPortableSkillInstall } from "@opengeni/db";
|
|
17
|
+
|
|
18
|
+
/** Canonical text-only folder; rejects invalid Unicode before PostgreSQL conversion. */
|
|
19
|
+
export function validateSkillFiles(files: readonly SkillFile[]): SkillFile[] {
|
|
20
|
+
validateSkillTextFiles(files);
|
|
21
|
+
const result = files.map(({ path, content }) => ({ path, content }));
|
|
22
|
+
if (!result.some((file) => file.path === "SKILL.md" && file.content.trim()))
|
|
23
|
+
throw new Error("Skill requires nonempty SKILL.md");
|
|
24
|
+
return result.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Separate from the historical SKILL.md hash, which must never be rewritten. */
|
|
28
|
+
export function skillBundleHash(files: readonly SkillFile[]): string {
|
|
29
|
+
return skillFilesContentHash(validateSkillFiles(files));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const listSkills = listSkillRecords;
|
|
33
|
+
export async function readSkill(
|
|
34
|
+
db: Database,
|
|
35
|
+
context: SkillReadContext,
|
|
36
|
+
skillId: string,
|
|
37
|
+
revisionId?: string,
|
|
38
|
+
) {
|
|
39
|
+
return (
|
|
40
|
+
(
|
|
41
|
+
await listSkillRecords(db, context, {
|
|
42
|
+
skillId,
|
|
43
|
+
...(revisionId ? { revisionId } : {}),
|
|
44
|
+
limit: 1,
|
|
45
|
+
})
|
|
46
|
+
)[0] ?? null
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
export async function saveSkill(db: Database, input: SkillSaveInput) {
|
|
50
|
+
const { accountId, workspaceId, actor, ...request } = input;
|
|
51
|
+
const artifact = buildPortableSkillArtifact(validateSkillFiles(input.files));
|
|
52
|
+
return applySkillLifecycle(
|
|
53
|
+
db,
|
|
54
|
+
{ accountId, workspaceId, actor },
|
|
55
|
+
{
|
|
56
|
+
...request,
|
|
57
|
+
operation: "save",
|
|
58
|
+
files: artifact.files,
|
|
59
|
+
title: artifact.name,
|
|
60
|
+
description: artifact.description,
|
|
61
|
+
},
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
export async function installSkill(db: Database, input: SkillInstallInput) {
|
|
65
|
+
const { accountId, workspaceId, actor, ...request } = input;
|
|
66
|
+
return applySkillLifecycle(
|
|
67
|
+
db,
|
|
68
|
+
{ accountId, workspaceId, actor },
|
|
69
|
+
{ ...request, operation: "install" },
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
export async function approveSkill(db: Database, input: SkillRevisionInput) {
|
|
73
|
+
const { accountId, workspaceId, actor, ...request } = input;
|
|
74
|
+
return applySkillLifecycle(
|
|
75
|
+
db,
|
|
76
|
+
{ accountId, workspaceId, actor },
|
|
77
|
+
{ ...request, operation: "approve" },
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
export async function restoreSkill(db: Database, input: SkillRevisionInput) {
|
|
81
|
+
const { accountId, workspaceId, actor, ...request } = input;
|
|
82
|
+
return applySkillLifecycle(
|
|
83
|
+
db,
|
|
84
|
+
{ accountId, workspaceId, actor },
|
|
85
|
+
{ ...request, operation: "restore" },
|
|
86
|
+
);
|
|
87
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export * from "./domain/skills";
|
|
1
2
|
// @opengeni/core — the framework-agnostic OpenGeni core.
|
|
2
3
|
//
|
|
3
4
|
// WHAT THIS PACKAGE IS: the OpenGeni domain, access, and billing layers carved
|
|
@@ -71,6 +72,8 @@ export * from "./billing/limits";
|
|
|
71
72
|
export * from "./domain/capabilities";
|
|
72
73
|
export * from "./domain/host-mcp-authority-source-admission";
|
|
73
74
|
export * from "./domain/skill-imports";
|
|
75
|
+
export * from "./domain/skill-search";
|
|
76
|
+
export * from "./domain/github-skill-source";
|
|
74
77
|
export * from "./domain/environments";
|
|
75
78
|
export * from "./rigs";
|
|
76
79
|
export * from "./domain/packs";
|
package/src/sandbox/routing.ts
CHANGED
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
// over the events bus) lives here, not in the leaf (which stays db-free).
|
|
13
13
|
|
|
14
14
|
import { sandboxLifecycleTransitionWaitMs, type Settings } from "@opengeni/config";
|
|
15
|
+
import { appendSessionCommandOutput } from "@opengeni/db/session-command-output";
|
|
16
|
+
import {
|
|
17
|
+
createProviderCommandRetainer,
|
|
18
|
+
retainedProviderCommandPersistence,
|
|
19
|
+
} from "@opengeni/db/retained-provider-commands";
|
|
15
20
|
import {
|
|
16
21
|
advanceWorkspaceGenerationForDirectRequest,
|
|
17
22
|
advanceWorkspaceGenerationForRetainedProcess,
|
|
@@ -21,6 +26,7 @@ import {
|
|
|
21
26
|
markWarmLeaseInstanceLost,
|
|
22
27
|
readActiveSandbox,
|
|
23
28
|
retainWorkspaceMutationProcess,
|
|
29
|
+
SandboxRetainedProcessPromotionFencedError,
|
|
24
30
|
retainedProcessSettlementIdentity,
|
|
25
31
|
settleRetainedProcess,
|
|
26
32
|
verifyDirectWorkspaceMutationSettlement,
|
|
@@ -58,6 +64,11 @@ type PersistableMutationAdmission = {
|
|
|
58
64
|
> | null;
|
|
59
65
|
};
|
|
60
66
|
|
|
67
|
+
const retainWorkspaceProviderCommand = createProviderCommandRetainer(
|
|
68
|
+
retainWorkspaceMutationProcess,
|
|
69
|
+
(error) => (error instanceof SandboxRetainedProcessPromotionFencedError ? error.process : null),
|
|
70
|
+
);
|
|
71
|
+
|
|
61
72
|
type DirectRetainedProcessRoute = {
|
|
62
73
|
providerSessionId: number;
|
|
63
74
|
providerBackend: string;
|
|
@@ -310,12 +321,15 @@ export function wrapChannelABoxWithRouting(
|
|
|
310
321
|
throw new Error("API-direct workspace mutation settlement lacked its bound admission");
|
|
311
322
|
}
|
|
312
323
|
if (outcome === "resolved" && retainedProcess) {
|
|
313
|
-
await
|
|
324
|
+
await retainWorkspaceProviderCommand(db, {
|
|
314
325
|
accountId: ids.accountId,
|
|
315
326
|
workspaceId: ids.workspaceId,
|
|
316
327
|
sessionId: ids.sessionId,
|
|
317
328
|
processId: retainedProcess.id,
|
|
318
329
|
providerSessionId: retainedProcess.providerSessionId,
|
|
330
|
+
...(retainedProcess.providerCommand
|
|
331
|
+
? { providerCommand: retainedProcess.providerCommand }
|
|
332
|
+
: {}),
|
|
319
333
|
admissionId: exactAdmission.id,
|
|
320
334
|
admittedWorkspaceGeneration: exactAdmission.workspaceGeneration,
|
|
321
335
|
operation: op,
|
|
@@ -516,6 +530,31 @@ export function wrapChannelABoxWithRouting(
|
|
|
516
530
|
});
|
|
517
531
|
|
|
518
532
|
const proxy = new RoutingSandboxSession({
|
|
533
|
+
providerCommandHandle: (value) =>
|
|
534
|
+
value && typeof value === "object"
|
|
535
|
+
? (value as PersistableMutationAdmission).admission?.workspaceGeneration
|
|
536
|
+
: undefined,
|
|
537
|
+
providerCommandPersistence: (process) =>
|
|
538
|
+
retainedProviderCommandPersistence(db, {
|
|
539
|
+
accountId: ids.accountId,
|
|
540
|
+
workspaceId: ids.workspaceId,
|
|
541
|
+
sessionId: ids.sessionId,
|
|
542
|
+
processId: process.id,
|
|
543
|
+
}),
|
|
544
|
+
captureProcessOutput: async ({ process, chunkId, chunk, stream, streamFidelity }) => {
|
|
545
|
+
const events = await appendSessionCommandOutput(db, {
|
|
546
|
+
accountId: ids.accountId,
|
|
547
|
+
workspaceId: ids.workspaceId,
|
|
548
|
+
sessionId: ids.sessionId,
|
|
549
|
+
commandId: process.id,
|
|
550
|
+
chunkId,
|
|
551
|
+
chunk,
|
|
552
|
+
stream,
|
|
553
|
+
streamFidelity,
|
|
554
|
+
});
|
|
555
|
+
if (events.length && bus)
|
|
556
|
+
await bus.publish(ids.workspaceId, ids.sessionId, events).catch(() => undefined);
|
|
557
|
+
},
|
|
519
558
|
bindActiveRouteOnFirstResolve: true,
|
|
520
559
|
defaultResolved: {
|
|
521
560
|
session: established.session as RoutableBackendSession,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Settings } from "@opengeni/config";
|
|
2
2
|
import {
|
|
3
3
|
CapabilityPack,
|
|
4
|
+
StoredCapabilityPack,
|
|
4
5
|
type RigVersion,
|
|
5
6
|
type Session,
|
|
6
7
|
type SandboxBackend,
|
|
@@ -72,8 +73,14 @@ export async function resolveWorkspaceLegacyRuntimePacks(
|
|
|
72
73
|
continue;
|
|
73
74
|
}
|
|
74
75
|
const registration = await getWorkspacePack(db, workspaceId, installation.packId);
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
if (!registration)
|
|
77
|
+
throw new Error(`Enabled Pack ${installation.packId} has no registered manifest`);
|
|
78
|
+
const parsed = StoredCapabilityPack.safeParse(registration.pack);
|
|
79
|
+
if (!parsed.success)
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Enabled Pack ${installation.packId} requires repair: ${parsed.error.message}`,
|
|
82
|
+
);
|
|
83
|
+
packs.push(parsed.data);
|
|
77
84
|
}
|
|
78
85
|
return packs;
|
|
79
86
|
}
|
package/src/transcription.ts
CHANGED
|
@@ -31,6 +31,9 @@ export type TranscriptionRequest = {
|
|
|
31
31
|
providerDeadlineAt?: Date | undefined;
|
|
32
32
|
/** Exact provider selected before a resumable segment is first sent upstream. */
|
|
33
33
|
providerId?: string | undefined;
|
|
34
|
+
preferredProvider?: string | null | undefined;
|
|
35
|
+
fallbackEnabled?: boolean | undefined;
|
|
36
|
+
excludedProviders?: readonly string[] | undefined;
|
|
34
37
|
};
|
|
35
38
|
|
|
36
39
|
export type TranscriptionResult = TranscribeAudioResponse & {
|
|
@@ -44,18 +47,22 @@ export class TranscriptionServiceError extends Error {
|
|
|
44
47
|
readonly code: VoiceInputErrorCode;
|
|
45
48
|
readonly status: number;
|
|
46
49
|
readonly retryable: boolean;
|
|
50
|
+
/** Explicit rejection before any transcription result; safe to try another provider. */
|
|
51
|
+
readonly fallbackSafe: boolean;
|
|
47
52
|
|
|
48
53
|
constructor(input: {
|
|
49
54
|
code: VoiceInputErrorCode;
|
|
50
55
|
message: string;
|
|
51
56
|
status?: number;
|
|
52
57
|
retryable?: boolean;
|
|
58
|
+
fallbackSafe?: boolean;
|
|
53
59
|
}) {
|
|
54
60
|
super(input.message);
|
|
55
61
|
this.name = "TranscriptionServiceError";
|
|
56
62
|
this.code = input.code;
|
|
57
63
|
this.status = input.status ?? statusForVoiceInputError(input.code);
|
|
58
64
|
this.retryable = input.retryable ?? false;
|
|
65
|
+
this.fallbackSafe = input.fallbackSafe ?? false;
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
68
|
|
|
@@ -88,6 +95,10 @@ export function statusForVoiceInputError(code: VoiceInputErrorCode): number {
|
|
|
88
95
|
|
|
89
96
|
/** Optional workspace scope for readiness checks during provider selection. */
|
|
90
97
|
export type TranscriptionAvailabilityContext = {
|
|
98
|
+
afterProvider?: string | undefined;
|
|
99
|
+
preferredProvider?: string | null | undefined;
|
|
100
|
+
fallbackEnabled?: boolean | undefined;
|
|
101
|
+
excludedProviders?: readonly string[] | undefined;
|
|
91
102
|
workspaceId?: string | undefined;
|
|
92
103
|
subjectId?: string | undefined;
|
|
93
104
|
};
|
|
@@ -95,7 +106,8 @@ export type TranscriptionAvailabilityContext = {
|
|
|
95
106
|
/**
|
|
96
107
|
* Extensible transcription provider port. Implementations own credentials and
|
|
97
108
|
* upstream request shape. Selection happens before audio is sent; providers must
|
|
98
|
-
*
|
|
109
|
+
* only fall back after an explicit rejection and before any successful or
|
|
110
|
+
* uncertain provider attempt. Recording persistence owns that eligibility.
|
|
99
111
|
*/
|
|
100
112
|
export type TranscriptionProvider = {
|
|
101
113
|
readonly id: string;
|