@toloka-platform/sdk 0.1.0-preview.4 → 0.1.0-preview.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli/toloka.js +132 -126
- package/dist/cli/toloka.js.jsc +0 -0
- package/dist/cli/toloka.js.jsc.sha256 +1 -1
- package/dist/cli/toloka.js.sha256 +1 -1
- package/docs/cli-v2.md +4 -0
- package/docs/cli.md +82 -6
- package/docs/datasets.md +8 -9
- package/docs/getting-started.md +2 -2
- package/docs/limitations.md +5 -8
- package/docs/troubleshooting.md +0 -11
- package/openapi/gen/api.ts +782 -876
- package/openapi/gen/models/baseModel.ts +9 -0
- package/openapi/gen/models/baseModelId.ts +2 -0
- package/openapi/gen/models/baseModelIdProperty.ts +2 -0
- package/openapi/gen/models/createDatasetBody.ts +2 -0
- package/openapi/gen/models/createDatasetBodyProfile.ts +2 -0
- package/openapi/gen/models/createDatasetUploadBodyFormat.ts +1 -1
- package/openapi/gen/models/createProjectModelBody.ts +8 -0
- package/openapi/gen/models/createProjectModelBodyDescription.ts +2 -0
- package/openapi/gen/models/dataset.ts +2 -0
- package/openapi/gen/models/datasetProfile.ts +3 -0
- package/openapi/gen/models/datasetProfileStamp.ts +6 -0
- package/openapi/gen/models/datasetProfileStampName.ts +2 -0
- package/openapi/gen/models/generationNodeSettingsAudienceAndPricingOneOfOneone.ts +1 -0
- package/openapi/gen/models/index.ts +21 -0
- package/openapi/gen/models/listBaseModels200.ts +5 -0
- package/openapi/gen/models/listModelVersions200.ts +7 -0
- package/openapi/gen/models/listModelVersionsParams.ts +5 -0
- package/openapi/gen/models/listProjectModels200.ts +7 -0
- package/openapi/gen/models/listProjectModelsParams.ts +5 -0
- package/openapi/gen/models/model.ts +16 -0
- package/openapi/gen/models/modelArchivedAt.ts +2 -0
- package/openapi/gen/models/modelDescription.ts +2 -0
- package/openapi/gen/models/modelId.ts +2 -0
- package/openapi/gen/models/modelVersion.ts +9 -0
- package/openapi/gen/models/modelVersionId.ts +2 -0
- package/openapi/gen/models/pipelineGraphSnapshotGraphNodesItemOneOfThreeoneSettingsAudienceAndPricingOneOfOneone.ts +1 -0
- package/openapi/gen/models/runCost.ts +7 -0
- package/openapi/gen/models/validatePipelineGraphBodyGraphNodesItemOneOfThreeoneSettingsAudienceAndPricingOneOfOneone.ts +1 -0
- package/package.json +2 -1
- package/skills/catalog.json +22 -0
- package/skills/{SKILL.md → toloka-build/SKILL.md} +2 -2
- package/skills/{references → toloka-build/references}/workflow-and-cli.md +26 -5
- package/skills/toloka-report-issue/SKILL.md +25 -0
- package/skills/toloka-report-issue/references/report-template.md +34 -0
- package/src/cli/analytics.ts +53 -1
- package/src/cli/data.ts +148 -25
- package/src/cli/file-system.ts +34 -0
- package/src/cli/format-error.ts +19 -0
- package/src/cli/init.ts +149 -194
- package/src/cli/models.ts +188 -0
- package/src/cli/program.ts +202 -22
- package/src/cli/report.ts +144 -0
- package/src/cli/skill.ts +363 -0
- package/src/client.ts +109 -35
- package/src/errors.ts +25 -10
- package/src/pipeline-graph-v1.contract.json +5 -1
- package/src/transport.ts +16 -13
- /package/skills/{references → toloka-build/references}/node-semantics.md +0 -0
package/src/cli/skill.ts
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { assert } from "es-toolkit";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { copyFile, mkdir, mkdtemp, readFile, realpath, rename, rm, stat } from "node:fs/promises";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { assertSafeDestination, entryStats, hasErrorCode } from "./file-system";
|
|
7
|
+
import { formatCliError } from "./format-error";
|
|
8
|
+
|
|
9
|
+
export const BUNDLED_SKILL_CATALOG_ROOT = fileURLToPath(new URL("../../skills", import.meta.url));
|
|
10
|
+
export const SKILL_CATALOG_MANIFEST = "catalog.json";
|
|
11
|
+
export const SKILL_INSTALL_ROOT = ".agents/skills";
|
|
12
|
+
export const SKILL_ENTRY_POINT = "SKILL.md";
|
|
13
|
+
|
|
14
|
+
const SKILL_CATALOG_DIRECTORY = "skills";
|
|
15
|
+
const MAX_SKILL_COUNT = 500;
|
|
16
|
+
const MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
17
|
+
const MAX_SKILL_ID_LENGTH = 64;
|
|
18
|
+
const SKILL_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
19
|
+
const DIGEST_PATTERN = /^[a-f0-9]{64}$/u;
|
|
20
|
+
|
|
21
|
+
export type SkillDescriptor = {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly description: string;
|
|
25
|
+
readonly tags: readonly string[];
|
|
26
|
+
readonly version: string;
|
|
27
|
+
readonly digest: string;
|
|
28
|
+
readonly files: readonly string[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type SkillCatalog = {
|
|
32
|
+
readonly root: string;
|
|
33
|
+
readonly manifestPath: string;
|
|
34
|
+
readonly skills: readonly SkillDescriptor[];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type SkillStatus = "not-installed" | "current" | "stale";
|
|
38
|
+
|
|
39
|
+
export type SkillListEntry = SkillDescriptor & { readonly status: SkillStatus };
|
|
40
|
+
|
|
41
|
+
export type InstalledSkill = {
|
|
42
|
+
readonly skill: SkillDescriptor;
|
|
43
|
+
readonly directory: string;
|
|
44
|
+
readonly replaced: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type InstallSkillOptions = {
|
|
48
|
+
readonly catalog: SkillCatalog;
|
|
49
|
+
readonly sourceRoot: string;
|
|
50
|
+
readonly skillId: string;
|
|
51
|
+
readonly force?: boolean;
|
|
52
|
+
readonly rename?: typeof rename;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type SkillIo = {
|
|
56
|
+
readonly stdout: (text: string) => void;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
60
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
61
|
+
|
|
62
|
+
const isStringArray = (value: unknown): value is string[] =>
|
|
63
|
+
Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
64
|
+
|
|
65
|
+
const isNonEmptyString = (value: unknown): value is string =>
|
|
66
|
+
typeof value === "string" && value.trim().length > 0;
|
|
67
|
+
|
|
68
|
+
const isSafeSkillId = (value: unknown): value is string =>
|
|
69
|
+
typeof value === "string" && value.length <= MAX_SKILL_ID_LENGTH && SKILL_ID_PATTERN.test(value);
|
|
70
|
+
|
|
71
|
+
const isSafeRelativePath = (path: string) =>
|
|
72
|
+
!path.includes("\\") &&
|
|
73
|
+
!path.includes("\0") &&
|
|
74
|
+
path.split("/").every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
|
|
75
|
+
|
|
76
|
+
const parseDescriptor = (value: unknown, invalid: (detail: string) => string): SkillDescriptor => {
|
|
77
|
+
assert(isRecord(value), invalid("every skill must be an object."));
|
|
78
|
+
const { id, name, description, tags, version, digest, files } = value;
|
|
79
|
+
assert(
|
|
80
|
+
isSafeSkillId(id),
|
|
81
|
+
invalid(
|
|
82
|
+
`skill id ${JSON.stringify(id)} must be at most ${MAX_SKILL_ID_LENGTH} lowercase letters, digits, and single hyphens.`,
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
assert(isNonEmptyString(name), invalid(`skill ${id} needs a name.`));
|
|
86
|
+
assert(isNonEmptyString(description), invalid(`skill ${id} needs a description.`));
|
|
87
|
+
assert(isStringArray(tags), invalid(`skill ${id} tags must be strings.`));
|
|
88
|
+
assert(isNonEmptyString(version), invalid(`skill ${id} needs a version.`));
|
|
89
|
+
assert(
|
|
90
|
+
typeof digest === "string" && DIGEST_PATTERN.test(digest),
|
|
91
|
+
invalid(`skill ${id} digest must be the sha256 hex digest of its files.`),
|
|
92
|
+
);
|
|
93
|
+
assert(
|
|
94
|
+
isStringArray(files) && files.includes(SKILL_ENTRY_POINT),
|
|
95
|
+
invalid(`skill ${id} files must include ${SKILL_ENTRY_POINT}.`),
|
|
96
|
+
);
|
|
97
|
+
assert(
|
|
98
|
+
files.every(isSafeRelativePath),
|
|
99
|
+
invalid(`skill ${id} files must be relative paths inside its directory.`),
|
|
100
|
+
);
|
|
101
|
+
assert(new Set(files).size === files.length, invalid(`skill ${id} lists a file more than once.`));
|
|
102
|
+
return { id, name, description, tags, version, digest, files };
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const parseManifest = (source: string, manifestPath: string): unknown => {
|
|
106
|
+
try {
|
|
107
|
+
return JSON.parse(source);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw new Error(`Invalid skill catalog ${manifestPath}: ${formatCliError(error)}`);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const isFileAt = async (path: string): Promise<boolean> => {
|
|
114
|
+
try {
|
|
115
|
+
return (await stat(path)).isFile();
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (hasErrorCode(error, "ENOENT", "ENOTDIR")) return false;
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const findSkillCatalogRootAbove = async (directory: string): Promise<string | undefined> => {
|
|
123
|
+
const candidate = join(directory, SKILL_CATALOG_DIRECTORY);
|
|
124
|
+
if (await isFileAt(join(candidate, SKILL_CATALOG_MANIFEST))) return candidate;
|
|
125
|
+
const parent = dirname(directory);
|
|
126
|
+
return parent === directory ? undefined : findSkillCatalogRootAbove(parent);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export async function resolveBundledSkillCatalogRoot(
|
|
130
|
+
runtimeEntry: string | undefined = process.argv[1],
|
|
131
|
+
): Promise<string> {
|
|
132
|
+
const entry = runtimeEntry === undefined ? undefined : await realpath(runtimeEntry);
|
|
133
|
+
const runtimeRoot =
|
|
134
|
+
entry === undefined ? undefined : await findSkillCatalogRootAbove(dirname(entry));
|
|
135
|
+
if (runtimeRoot !== undefined) return runtimeRoot;
|
|
136
|
+
const searched =
|
|
137
|
+
entry === undefined
|
|
138
|
+
? "process.argv[1] names no runtime entry"
|
|
139
|
+
: `none exists between ${entry} and the filesystem root`;
|
|
140
|
+
assert(
|
|
141
|
+
await isFileAt(join(BUNDLED_SKILL_CATALOG_ROOT, SKILL_CATALOG_MANIFEST)),
|
|
142
|
+
`Cannot locate the bundled skill catalog. An installed @toloka-platform/sdk must ship ${SKILL_CATALOG_DIRECTORY}/${SKILL_CATALOG_MANIFEST} in a directory above its runtime entry, but ${searched}, and the source checkout catalog ${BUNDLED_SKILL_CATALOG_ROOT} is absent. Reinstall @toloka-platform/sdk.`,
|
|
143
|
+
);
|
|
144
|
+
return BUNDLED_SKILL_CATALOG_ROOT;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const loadBundledSkillCatalog = async (): Promise<SkillCatalog> =>
|
|
148
|
+
loadSkillCatalog(await resolveBundledSkillCatalogRoot());
|
|
149
|
+
|
|
150
|
+
export async function loadSkillCatalog(catalogRoot: string): Promise<SkillCatalog> {
|
|
151
|
+
const manifestPath = join(catalogRoot, SKILL_CATALOG_MANIFEST);
|
|
152
|
+
const invalid = (detail: string) => `Invalid skill catalog ${manifestPath}: ${detail}`;
|
|
153
|
+
const { size } = await stat(manifestPath);
|
|
154
|
+
assert(
|
|
155
|
+
size <= MAX_MANIFEST_BYTES,
|
|
156
|
+
invalid(`the manifest is ${size} bytes; the limit is ${MAX_MANIFEST_BYTES} bytes.`),
|
|
157
|
+
);
|
|
158
|
+
const manifest = parseManifest(await readFile(manifestPath, "utf8"), manifestPath);
|
|
159
|
+
assert(
|
|
160
|
+
isRecord(manifest) && Array.isArray(manifest.skills),
|
|
161
|
+
invalid("expected an object with a skills array."),
|
|
162
|
+
);
|
|
163
|
+
assert(
|
|
164
|
+
manifest.skills.length <= MAX_SKILL_COUNT,
|
|
165
|
+
invalid(`${manifest.skills.length} skills exceed the limit of ${MAX_SKILL_COUNT}.`),
|
|
166
|
+
);
|
|
167
|
+
const skills = manifest.skills.map((skill) => parseDescriptor(skill, invalid));
|
|
168
|
+
const ids = new Set<string>();
|
|
169
|
+
for (const skill of skills) {
|
|
170
|
+
assert(!ids.has(skill.id), invalid(`skill id ${skill.id} appears more than once.`));
|
|
171
|
+
ids.add(skill.id);
|
|
172
|
+
}
|
|
173
|
+
return { root: catalogRoot, manifestPath, skills };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function computeSkillDigest(
|
|
177
|
+
directory: string,
|
|
178
|
+
files: readonly string[],
|
|
179
|
+
): Promise<string> {
|
|
180
|
+
const hash = createHash("sha256");
|
|
181
|
+
for (const path of files.toSorted()) {
|
|
182
|
+
hash.update(path);
|
|
183
|
+
hash.update("\0");
|
|
184
|
+
hash.update(await readFile(join(directory, path)));
|
|
185
|
+
hash.update("\0");
|
|
186
|
+
}
|
|
187
|
+
return hash.digest("hex");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const searchSkills = (
|
|
191
|
+
skills: readonly SkillDescriptor[],
|
|
192
|
+
query: string,
|
|
193
|
+
): SkillDescriptor[] => {
|
|
194
|
+
const needle = query.trim().toLowerCase();
|
|
195
|
+
return skills.filter((skill) =>
|
|
196
|
+
[skill.id, skill.name, skill.description, ...skill.tags].some((field) =>
|
|
197
|
+
field.toLowerCase().includes(needle),
|
|
198
|
+
),
|
|
199
|
+
);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export async function skillStatus(
|
|
203
|
+
sourceRoot: string,
|
|
204
|
+
skill: SkillDescriptor,
|
|
205
|
+
): Promise<SkillStatus> {
|
|
206
|
+
const directory = join(sourceRoot, SKILL_INSTALL_ROOT, skill.id);
|
|
207
|
+
if ((await entryStats(directory)) === undefined) return "not-installed";
|
|
208
|
+
try {
|
|
209
|
+
return (await computeSkillDigest(directory, skill.files)) === skill.digest
|
|
210
|
+
? "current"
|
|
211
|
+
: "stale";
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (hasErrorCode(error, "ENOENT", "ENOTDIR")) return "stale";
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export const listSkills = ({
|
|
219
|
+
sourceRoot,
|
|
220
|
+
skills,
|
|
221
|
+
}: {
|
|
222
|
+
readonly sourceRoot: string;
|
|
223
|
+
readonly skills: readonly SkillDescriptor[];
|
|
224
|
+
}): Promise<SkillListEntry[]> =>
|
|
225
|
+
Promise.all(
|
|
226
|
+
skills.map(async (skill) => ({ ...skill, status: await skillStatus(sourceRoot, skill) })),
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
const swapDirectories = async (
|
|
230
|
+
renameEntry: typeof rename,
|
|
231
|
+
replacement: string,
|
|
232
|
+
destination: string,
|
|
233
|
+
backup: string,
|
|
234
|
+
) => {
|
|
235
|
+
await renameEntry(destination, backup);
|
|
236
|
+
try {
|
|
237
|
+
await renameEntry(replacement, destination);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
try {
|
|
240
|
+
await renameEntry(backup, destination);
|
|
241
|
+
} catch (restoreError) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`Failed to replace ${destination}; the previous version is preserved at ${backup}. ${formatCliError(restoreError)}`,
|
|
244
|
+
{ cause: error },
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
await rm(backup, { recursive: true, force: true });
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export async function installSkill({
|
|
253
|
+
catalog,
|
|
254
|
+
sourceRoot,
|
|
255
|
+
skillId,
|
|
256
|
+
force = false,
|
|
257
|
+
rename: renameEntry = rename,
|
|
258
|
+
}: InstallSkillOptions): Promise<InstalledSkill> {
|
|
259
|
+
const skill = catalog.skills.find((candidate) => candidate.id === skillId);
|
|
260
|
+
assert(
|
|
261
|
+
skill,
|
|
262
|
+
`Skill ${JSON.stringify(skillId)} is not in the catalog. Run \`toloka skill list\` to see the available skills.`,
|
|
263
|
+
);
|
|
264
|
+
const relativeDestination = `${SKILL_INSTALL_ROOT}/${skill.id}`;
|
|
265
|
+
await assertSafeDestination(sourceRoot, relativeDestination);
|
|
266
|
+
const destination = join(sourceRoot, relativeDestination);
|
|
267
|
+
const existing = await entryStats(destination);
|
|
268
|
+
assert(
|
|
269
|
+
existing === undefined || force,
|
|
270
|
+
`${relativeDestination} already exists. Re-run with --force to replace it with the catalog version of ${skill.id}.`,
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
const skillsRoot = dirname(destination);
|
|
274
|
+
await mkdir(skillsRoot, { recursive: true });
|
|
275
|
+
const temporary = await mkdtemp(join(skillsRoot, `.${skill.id}.tmp-`));
|
|
276
|
+
try {
|
|
277
|
+
for (const path of skill.files) {
|
|
278
|
+
const target = join(temporary, path);
|
|
279
|
+
await mkdir(dirname(target), { recursive: true });
|
|
280
|
+
await copyFile(join(catalog.root, skill.id, path), target);
|
|
281
|
+
}
|
|
282
|
+
assert(
|
|
283
|
+
(await computeSkillDigest(temporary, skill.files)) === skill.digest,
|
|
284
|
+
`Skill ${skill.id} failed its digest check: the packaged files do not match the catalog digest ${skill.digest}. Nothing was installed.`,
|
|
285
|
+
);
|
|
286
|
+
if (existing === undefined) {
|
|
287
|
+
await renameEntry(temporary, destination);
|
|
288
|
+
} else {
|
|
289
|
+
await swapDirectories(renameEntry, temporary, destination, `${temporary}.backup`);
|
|
290
|
+
}
|
|
291
|
+
} catch (error) {
|
|
292
|
+
await rm(temporary, { recursive: true, force: true });
|
|
293
|
+
throw error;
|
|
294
|
+
}
|
|
295
|
+
return { skill, directory: destination, replaced: existing !== undefined };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const formatEntries = (entries: readonly SkillListEntry[], json: boolean): string => {
|
|
299
|
+
if (json) return JSON.stringify(entries, undefined, 2);
|
|
300
|
+
const idWidth = Math.max(...entries.map((entry) => entry.id.length));
|
|
301
|
+
const statusWidth = Math.max(...entries.map((entry) => entry.status.length));
|
|
302
|
+
return entries
|
|
303
|
+
.flatMap((entry) => [
|
|
304
|
+
`${entry.id.padEnd(idWidth)} ${entry.status.padEnd(statusWidth)} ${entry.name}`,
|
|
305
|
+
` ${entry.description}`,
|
|
306
|
+
])
|
|
307
|
+
.join("\n");
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
export async function skillListCommand({
|
|
311
|
+
sourceRoot,
|
|
312
|
+
json,
|
|
313
|
+
io,
|
|
314
|
+
}: {
|
|
315
|
+
readonly sourceRoot: string;
|
|
316
|
+
readonly json: boolean;
|
|
317
|
+
readonly io: SkillIo;
|
|
318
|
+
}): Promise<void> {
|
|
319
|
+
const catalog = await loadBundledSkillCatalog();
|
|
320
|
+
io.stdout(formatEntries(await listSkills({ sourceRoot, skills: catalog.skills }), json));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export async function skillSearchCommand({
|
|
324
|
+
sourceRoot,
|
|
325
|
+
query,
|
|
326
|
+
json,
|
|
327
|
+
io,
|
|
328
|
+
}: {
|
|
329
|
+
readonly sourceRoot: string;
|
|
330
|
+
readonly query: string;
|
|
331
|
+
readonly json: boolean;
|
|
332
|
+
readonly io: SkillIo;
|
|
333
|
+
}): Promise<void> {
|
|
334
|
+
const catalog = await loadBundledSkillCatalog();
|
|
335
|
+
const matches = await listSkills({ sourceRoot, skills: searchSkills(catalog.skills, query) });
|
|
336
|
+
if (json || matches.length > 0) {
|
|
337
|
+
io.stdout(formatEntries(matches, json));
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
io.stdout(
|
|
341
|
+
`No skills match ${JSON.stringify(query)}. Run \`toloka skill list\` to see the catalog.`,
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export async function skillInstallCommand({
|
|
346
|
+
sourceRoot,
|
|
347
|
+
skillId,
|
|
348
|
+
force,
|
|
349
|
+
io,
|
|
350
|
+
}: {
|
|
351
|
+
readonly sourceRoot: string;
|
|
352
|
+
readonly skillId: string;
|
|
353
|
+
readonly force: boolean;
|
|
354
|
+
readonly io: SkillIo;
|
|
355
|
+
}): Promise<void> {
|
|
356
|
+
const catalog = await loadBundledSkillCatalog();
|
|
357
|
+
const installed = await installSkill({ catalog, sourceRoot, skillId, force });
|
|
358
|
+
const relativeDestination = `${SKILL_INSTALL_ROOT}/${installed.skill.id}`;
|
|
359
|
+
const fileCount = installed.skill.files.length;
|
|
360
|
+
io.stdout(
|
|
361
|
+
`${installed.replaced ? "Replaced" : "Installed"} ${installed.skill.id} in ${relativeDestination} (${fileCount} ${fileCount === 1 ? "file" : "files"}). Start with ${relativeDestination}/${SKILL_ENTRY_POINT}.`,
|
|
362
|
+
);
|
|
363
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
CopyPipelineBody,
|
|
9
9
|
CreateDatasetBody,
|
|
10
10
|
CreateDatasetExportBody,
|
|
11
|
+
CreateProjectModelBody,
|
|
11
12
|
CreateDatasetFieldBodyAnyOf,
|
|
12
13
|
CreateDatasetUploadBody,
|
|
13
14
|
CreatePipelineCheckBody,
|
|
@@ -21,13 +22,17 @@ import type {
|
|
|
21
22
|
ImportPipelineBody,
|
|
22
23
|
InferredField as GeneratedInferredField,
|
|
23
24
|
ListDatasetFieldsParams,
|
|
25
|
+
ListModelVersionsParams,
|
|
24
26
|
ListDatasetItemsParams,
|
|
25
27
|
ListPipelineRunItemsParams,
|
|
26
28
|
ListPipelineRunsParams,
|
|
27
29
|
ListProjectDatasetsParams,
|
|
28
30
|
ListProjectPipelinesParams,
|
|
31
|
+
ListProjectModelsParams,
|
|
29
32
|
ListProjectsParams,
|
|
30
33
|
Pipeline as GeneratedPipeline,
|
|
34
|
+
Model as GeneratedModel,
|
|
35
|
+
ModelVersion as GeneratedModelVersion,
|
|
31
36
|
PatchPipelineNodeBody,
|
|
32
37
|
PipelineGraphSnapshot,
|
|
33
38
|
Project as GeneratedProject,
|
|
@@ -38,6 +43,8 @@ import type {
|
|
|
38
43
|
RunJob as GeneratedRunJob,
|
|
39
44
|
RunSummary as GeneratedRunSummary,
|
|
40
45
|
SetProjectSecretBody,
|
|
46
|
+
BaseModel as GeneratedBaseModel,
|
|
47
|
+
BaseModelId as GeneratedBaseModelId,
|
|
41
48
|
UpdatePipelineBody,
|
|
42
49
|
UpdateProjectBody,
|
|
43
50
|
UploadJob as GeneratedUploadJob,
|
|
@@ -57,6 +64,8 @@ export type DatasetFieldId = Branded<string, "DatasetFieldId">;
|
|
|
57
64
|
export type DatasetId = Branded<string, "DatasetId">;
|
|
58
65
|
export type DatasetItemId = Branded<string, "DatasetItemId">;
|
|
59
66
|
export type ExportId = Branded<string, "ExportId">;
|
|
67
|
+
export type ModelId = Branded<string, "ModelId">;
|
|
68
|
+
export type ModelVersionId = Branded<string, "ModelVersionId">;
|
|
60
69
|
export type PipelineId = Branded<string, "PipelineId">;
|
|
61
70
|
export type ProjectFileId = Branded<string, "ProjectFileId">;
|
|
62
71
|
export type ProjectId = Branded<string, "ProjectId">;
|
|
@@ -76,6 +85,8 @@ type ResourceIdsByProperty = {
|
|
|
76
85
|
exportId: ExportId;
|
|
77
86
|
fieldId: DatasetFieldId;
|
|
78
87
|
fileId: ProjectFileId;
|
|
88
|
+
modelId: ModelId;
|
|
89
|
+
modelVersionId: ModelVersionId;
|
|
79
90
|
goldenDatasetId: DatasetId;
|
|
80
91
|
itemId: RunItemId;
|
|
81
92
|
pipelineId: PipelineId;
|
|
@@ -121,6 +132,10 @@ export type DatasetField = BrandedResource<GeneratedDatasetField, DatasetFieldId
|
|
|
121
132
|
export type DatasetItem = BrandedResource<GeneratedDatasetItem, DatasetItemId>;
|
|
122
133
|
export type ExportJob = BrandedResource<GeneratedExportJob, ExportId>;
|
|
123
134
|
export type InferredField = GeneratedInferredField;
|
|
135
|
+
export type Model = BrandedResource<GeneratedModel, ModelId>;
|
|
136
|
+
export type ModelVersion = BrandedResource<GeneratedModelVersion, ModelVersionId>;
|
|
137
|
+
export type BaseModel = GeneratedBaseModel;
|
|
138
|
+
export type BaseModelId = GeneratedBaseModelId;
|
|
124
139
|
export type Pipeline = BrandedResource<GeneratedPipeline, PipelineId>;
|
|
125
140
|
export type Project = BrandedResource<GeneratedProject, ProjectId>;
|
|
126
141
|
export type ProjectFile = BrandedResource<GeneratedProjectFile, ProjectFileId>;
|
|
@@ -178,6 +193,12 @@ export type WaitForTarget =
|
|
|
178
193
|
const createClientImplementation = (config: ClientConfig) => {
|
|
179
194
|
const requestOptions = createRequestOptions(config.baseUrl, config.apiKey, config.fetch);
|
|
180
195
|
|
|
196
|
+
const emptyJsonRequestOptions = (): RequestInit => {
|
|
197
|
+
const headers = new Headers(requestOptions.headers);
|
|
198
|
+
headers.set("Content-Type", "application/json");
|
|
199
|
+
return { ...requestOptions, headers, body: "{}" };
|
|
200
|
+
};
|
|
201
|
+
|
|
181
202
|
function waitFor(
|
|
182
203
|
target: DatasetUploadWaitTarget,
|
|
183
204
|
options?: WaitForOptions,
|
|
@@ -296,6 +317,56 @@ const createClientImplementation = (config: ClientConfig) => {
|
|
|
296
317
|
wire.copyPipeline(pipelineId, body, requestOptions),
|
|
297
318
|
};
|
|
298
319
|
|
|
320
|
+
const baseModels = {
|
|
321
|
+
list: async () => (await wire.listBaseModels(requestOptions)).baseModels,
|
|
322
|
+
get: ({ id }: { id: BaseModelId }) => wire.getBaseModel(id, requestOptions),
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const models = {
|
|
326
|
+
listPage: ({ projectId, ...params }: { projectId: ProjectId } & ListProjectModelsParams) =>
|
|
327
|
+
wire.listProjectModels(projectId, params, requestOptions),
|
|
328
|
+
listAll: ({ projectId, ...params }: { projectId: ProjectId } & ListProjectModelsParams) =>
|
|
329
|
+
listAllPages(async (pageOptions) => {
|
|
330
|
+
const page = await wire.listProjectModels(
|
|
331
|
+
projectId,
|
|
332
|
+
{ ...params, ...pageOptions },
|
|
333
|
+
requestOptions,
|
|
334
|
+
);
|
|
335
|
+
return { items: page.models, pagination: page.pagination };
|
|
336
|
+
}, params),
|
|
337
|
+
get: ({ projectId, modelId }: { projectId: ProjectId; modelId: ModelId }) =>
|
|
338
|
+
wire.getProjectModel(projectId, modelId, requestOptions),
|
|
339
|
+
listVersionsPage: ({
|
|
340
|
+
projectId,
|
|
341
|
+
modelId,
|
|
342
|
+
...params
|
|
343
|
+
}: { projectId: ProjectId; modelId: ModelId } & ListModelVersionsParams) =>
|
|
344
|
+
wire.listModelVersions(projectId, modelId, params, requestOptions),
|
|
345
|
+
listAllVersions: ({
|
|
346
|
+
projectId,
|
|
347
|
+
modelId,
|
|
348
|
+
...params
|
|
349
|
+
}: { projectId: ProjectId; modelId: ModelId } & ListModelVersionsParams) =>
|
|
350
|
+
listAllPages(async (pageOptions) => {
|
|
351
|
+
const page = await wire.listModelVersions(
|
|
352
|
+
projectId,
|
|
353
|
+
modelId,
|
|
354
|
+
{ ...params, ...pageOptions },
|
|
355
|
+
requestOptions,
|
|
356
|
+
);
|
|
357
|
+
return { items: page.versions, pagination: page.pagination };
|
|
358
|
+
}, params),
|
|
359
|
+
create: ({ projectId, ...body }: { projectId: ProjectId } & CreateProjectModelBody) =>
|
|
360
|
+
wire.createProjectModel(projectId, body, requestOptions),
|
|
361
|
+
getVersion: ({ modelVersionId }: { modelVersionId: ModelVersionId }) =>
|
|
362
|
+
wire.getModelVersion(modelVersionId, requestOptions),
|
|
363
|
+
archive: ({ projectId, modelId }: { projectId: ProjectId; modelId: ModelId }) => {
|
|
364
|
+
const headers = new Headers(requestOptions.headers);
|
|
365
|
+
headers.set("Content-Type", "application/json");
|
|
366
|
+
return wire.archiveProjectModel(projectId, modelId, { ...requestOptions, headers });
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
|
|
299
370
|
const datasets = {
|
|
300
371
|
create: ({ projectId, ...body }: { projectId: ProjectId } & CreateDatasetBody) =>
|
|
301
372
|
wire.createDataset(projectId, body, requestOptions),
|
|
@@ -363,7 +434,9 @@ const createClientImplementation = (config: ClientConfig) => {
|
|
|
363
434
|
startUpload: ({ datasetId, ...body }: { datasetId: DatasetId } & CreateDatasetUploadBody) =>
|
|
364
435
|
wire.createDatasetUpload(datasetId, body, requestOptions),
|
|
365
436
|
finalizeUpload: ({ uploadId }: { uploadId: UploadId }) =>
|
|
366
|
-
wire.finalizeDatasetUpload(uploadId,
|
|
437
|
+
wire.finalizeDatasetUpload(uploadId, emptyJsonRequestOptions()),
|
|
438
|
+
cancelUpload: ({ uploadId }: { uploadId: UploadId }) =>
|
|
439
|
+
wire.cancelDatasetUpload(uploadId, emptyJsonRequestOptions()),
|
|
367
440
|
getUpload: ({ uploadId }: { uploadId: UploadId }) =>
|
|
368
441
|
wire.getDatasetUpload(uploadId, requestOptions),
|
|
369
442
|
startExport: ({ datasetId, ...body }: { datasetId: DatasetId } & CreateDatasetExportBody) =>
|
|
@@ -373,15 +446,8 @@ const createClientImplementation = (config: ClientConfig) => {
|
|
|
373
446
|
};
|
|
374
447
|
|
|
375
448
|
const runs = {
|
|
376
|
-
start: ({ pipelineId }: { pipelineId: PipelineId }) =>
|
|
377
|
-
|
|
378
|
-
headers.set("Content-Type", "application/json");
|
|
379
|
-
return wire.createPipelineRun(pipelineId, {
|
|
380
|
-
...requestOptions,
|
|
381
|
-
headers,
|
|
382
|
-
body: "{}",
|
|
383
|
-
});
|
|
384
|
-
},
|
|
449
|
+
start: ({ pipelineId }: { pipelineId: PipelineId }) =>
|
|
450
|
+
wire.createPipelineRun(pipelineId, emptyJsonRequestOptions()),
|
|
385
451
|
get: ({ runId }: { runId: RunId }) => wire.getPipelineRun(runId, requestOptions),
|
|
386
452
|
cancel: ({ runId }: { runId: RunId }) => wire.cancelPipelineRun(runId, requestOptions),
|
|
387
453
|
listPage: ({ pipelineId, ...params }: { pipelineId: PipelineId } & ListPipelineRunsParams) =>
|
|
@@ -446,6 +512,8 @@ const createClientImplementation = (config: ClientConfig) => {
|
|
|
446
512
|
projects,
|
|
447
513
|
pipelines,
|
|
448
514
|
datasets,
|
|
515
|
+
baseModels,
|
|
516
|
+
models,
|
|
449
517
|
runs,
|
|
450
518
|
secrets,
|
|
451
519
|
files,
|
|
@@ -462,31 +530,37 @@ type PublicOutput<T> = T extends GeneratedCheckJob
|
|
|
462
530
|
? ExportJob
|
|
463
531
|
: T extends GeneratedUploadJob
|
|
464
532
|
? UploadJob
|
|
465
|
-
: T extends
|
|
466
|
-
?
|
|
467
|
-
: T extends
|
|
468
|
-
?
|
|
469
|
-
: T extends
|
|
470
|
-
?
|
|
471
|
-
: T extends
|
|
472
|
-
?
|
|
473
|
-
: T extends
|
|
474
|
-
?
|
|
475
|
-
: T extends
|
|
476
|
-
?
|
|
477
|
-
: T extends
|
|
478
|
-
?
|
|
479
|
-
: T extends
|
|
480
|
-
?
|
|
481
|
-
: T extends
|
|
482
|
-
?
|
|
483
|
-
: T extends
|
|
484
|
-
?
|
|
485
|
-
: T extends
|
|
486
|
-
?
|
|
487
|
-
: T extends
|
|
488
|
-
?
|
|
489
|
-
: T
|
|
533
|
+
: T extends GeneratedModel
|
|
534
|
+
? Model
|
|
535
|
+
: T extends GeneratedModelVersion
|
|
536
|
+
? ModelVersion
|
|
537
|
+
: T extends GeneratedProject
|
|
538
|
+
? Project
|
|
539
|
+
: T extends GeneratedPipeline
|
|
540
|
+
? Pipeline
|
|
541
|
+
: T extends GeneratedDataset
|
|
542
|
+
? Dataset
|
|
543
|
+
: T extends GeneratedDatasetField
|
|
544
|
+
? DatasetField
|
|
545
|
+
: T extends GeneratedDatasetItem
|
|
546
|
+
? DatasetItem
|
|
547
|
+
: T extends GeneratedProjectFile
|
|
548
|
+
? ProjectFile
|
|
549
|
+
: T extends GeneratedRunItem
|
|
550
|
+
? RunItem
|
|
551
|
+
: T extends GeneratedRunSummary
|
|
552
|
+
? RunSummary
|
|
553
|
+
: T extends Blob
|
|
554
|
+
? Blob
|
|
555
|
+
: T extends Promise<infer Value>
|
|
556
|
+
? Promise<PublicOutput<Value>>
|
|
557
|
+
: T extends AsyncGenerator<infer Value, infer Return, infer Next>
|
|
558
|
+
? AsyncGenerator<PublicOutput<Value>, PublicOutput<Return>, Next>
|
|
559
|
+
: T extends readonly unknown[]
|
|
560
|
+
? { [K in keyof T]: PublicOutput<T[K]> }
|
|
561
|
+
: T extends object
|
|
562
|
+
? { [K in keyof T]: PublicOutput<T[K]> }
|
|
563
|
+
: T;
|
|
490
564
|
|
|
491
565
|
type PublicClientMember<T> = T extends (...args: infer Args) => infer Result
|
|
492
566
|
? (...args: { [K in keyof Args]: PublicInput<Args[K]> }) => PublicOutput<Result>
|
package/src/errors.ts
CHANGED
|
@@ -1,28 +1,43 @@
|
|
|
1
|
-
export type PotatoApiErrorCode =
|
|
1
|
+
export type PotatoApiErrorCode = "http" | "wait_timeout";
|
|
2
2
|
|
|
3
3
|
export class PotatoApiError extends Error {
|
|
4
4
|
readonly code: PotatoApiErrorCode;
|
|
5
5
|
readonly status: number | undefined;
|
|
6
6
|
readonly body: unknown;
|
|
7
|
+
readonly requestId: string | undefined;
|
|
7
8
|
|
|
8
|
-
private constructor(
|
|
9
|
+
private constructor(
|
|
10
|
+
message: string,
|
|
11
|
+
code: PotatoApiErrorCode,
|
|
12
|
+
status: number | undefined,
|
|
13
|
+
body: unknown,
|
|
14
|
+
requestId?: string,
|
|
15
|
+
) {
|
|
9
16
|
super(message);
|
|
10
|
-
this.name =
|
|
17
|
+
this.name = "PotatoApiError";
|
|
11
18
|
this.code = code;
|
|
12
19
|
this.status = status;
|
|
13
20
|
this.body = body;
|
|
21
|
+
this.requestId = requestId;
|
|
14
22
|
}
|
|
15
23
|
|
|
16
|
-
static fromResponse(status: number, body: unknown) {
|
|
17
|
-
|
|
24
|
+
static fromResponse(status: number, body: unknown, requestId?: string) {
|
|
25
|
+
const requestSuffix = requestId === undefined ? "" : ` (request id: ${requestId})`;
|
|
26
|
+
return new PotatoApiError(
|
|
27
|
+
`Potato API request failed with status ${status}${requestSuffix}`,
|
|
28
|
+
"http",
|
|
29
|
+
status,
|
|
30
|
+
body,
|
|
31
|
+
requestId,
|
|
32
|
+
);
|
|
18
33
|
}
|
|
19
34
|
|
|
20
35
|
static waitTimeout(timeoutMs: number) {
|
|
21
36
|
return new PotatoApiError(
|
|
22
37
|
`Potato API job did not reach a terminal status within ${timeoutMs} ms`,
|
|
23
|
-
|
|
38
|
+
"wait_timeout",
|
|
24
39
|
undefined,
|
|
25
|
-
{ timeoutMs }
|
|
40
|
+
{ timeoutMs },
|
|
26
41
|
);
|
|
27
42
|
}
|
|
28
43
|
}
|
|
@@ -33,17 +48,17 @@ export type PipelineGraphValidationIssue = {
|
|
|
33
48
|
};
|
|
34
49
|
|
|
35
50
|
export class PipelineGraphValidationError extends Error {
|
|
36
|
-
readonly code =
|
|
51
|
+
readonly code = "invalid_pipeline_graph";
|
|
37
52
|
readonly issues: readonly PipelineGraphValidationIssue[];
|
|
38
53
|
|
|
39
54
|
private constructor(message: string, issues: readonly PipelineGraphValidationIssue[]) {
|
|
40
55
|
super(message);
|
|
41
|
-
this.name =
|
|
56
|
+
this.name = "PipelineGraphValidationError";
|
|
42
57
|
this.issues = issues;
|
|
43
58
|
}
|
|
44
59
|
|
|
45
60
|
static fromIssues(issues: readonly PipelineGraphValidationIssue[]) {
|
|
46
|
-
const details = issues.map(({ path, reason }) => `- ${path}: ${reason}`).join(
|
|
61
|
+
const details = issues.map(({ path, reason }) => `- ${path}: ${reason}`).join("\n");
|
|
47
62
|
return new PipelineGraphValidationError(`Pipeline graph validation failed\n${details}`, issues);
|
|
48
63
|
}
|
|
49
64
|
}
|
|
@@ -1633,10 +1633,14 @@
|
|
|
1633
1633
|
"llmInstruction": {
|
|
1634
1634
|
"default": "",
|
|
1635
1635
|
"type": "string"
|
|
1636
|
+
},
|
|
1637
|
+
"maxCostPerAttempt": {
|
|
1638
|
+
"type": "number"
|
|
1636
1639
|
}
|
|
1637
1640
|
},
|
|
1638
1641
|
"required": [
|
|
1639
|
-
"audienceType"
|
|
1642
|
+
"audienceType",
|
|
1643
|
+
"maxCostPerAttempt"
|
|
1640
1644
|
],
|
|
1641
1645
|
"additionalProperties": false
|
|
1642
1646
|
}
|