@toloka-platform/sdk 0.15.0 → 0.16.0

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.
Binary file
@@ -1 +1 @@
1
- d485d368046ed3f4e57bef3b0af760da04421a9be9e6e9f7927fb2e30dcd115d toloka.js.jsc
1
+ d657cb43672cd6deb574d75ed2a1c7b4a14d36d5b02a833d118bb0cd8d6634e7 toloka.js.jsc
@@ -1 +1 @@
1
- c42abdb877c4bca4d85c8d2faf266f640cbcf0b283d725482d1da3dfc753487f toloka.js
1
+ 59d0e496f8f496fc0fe031360beb5ee033d0eff186c7004914a659d904483cf6 toloka.js
package/docs/cli.md CHANGED
@@ -286,7 +286,7 @@ toloka validate [--wait]
286
286
 
287
287
  It prints one line per pipeline with its status, then its issues: `valid`; `invalid`; `checks-pending` when only check state is open, such as a stored test case or a lint that has not run on this graph; `checks-running` when `--wait` gave up on running checks; or `skipped` when the pipeline or its dataset is not on the platform yet. A `note:` line explains what to do next when `--wait` could not finish. Exits `1` when any submitted graph is invalid or its checks were still running at the `--wait` deadline.
288
288
 
289
- The platform lints a pipeline a few seconds after every apply. While a lint or a check is running, or a lint is about to start after an apply, validate lists it as `running running_node_check`; a lint that has not run on this graph and is no longer expected to start is `pending not_ran_node_check`, and the user runs it from the node's Checks tab. `--wait` validates again every 5 seconds until nothing is running, for up to 5 minutes, then prints the final report; a pipeline whose source differs from the applied graph is not waited on, because its lints start only after `toloka apply`. Right after an apply, plain `toloka validate` can print `valid` and exit `0` while lints are still running, so run `toloka validate --wait` before treating an applied pipeline as valid.
289
+ The platform lints a pipeline a few seconds after every apply. While a lint or a check is running, or a lint is about to start after an apply, validate lists it as `running running_node_check`; a lint that has not run on this graph and is no longer expected to start is `pending not_ran_node_check`; start it with `toloka pipeline lint`. `--wait` validates again every 5 seconds until nothing is running, for up to 5 minutes, then prints the final report; a pipeline whose source differs from the applied graph is not waited on, because its lints start only after `toloka apply`. Right after an apply, plain `toloka validate` can print `valid` and exit `0` while lints are still running, so run `toloka validate --wait` before treating an applied pipeline as valid.
290
290
 
291
291
  ## check
292
292
 
@@ -338,6 +338,7 @@ Operate on a live pipeline in the selected project. Name or id is required unles
338
338
 
339
339
  ```sh
340
340
  toloka pipeline list [--limit <n>]
341
+ toloka pipeline lint [name] [--wait] [--timeout <ms>]
341
342
  toloka pipeline run [name] [--wait] [--timeout <ms>]
342
343
  toloka pipeline status [name]
343
344
  toloka pipeline runs [name] [--limit <n>]
@@ -350,6 +351,10 @@ toloka pipeline results <runId> [--limit <n>] [--failed]
350
351
 
351
352
  `pipeline run` without `--wait` prints `Started run <id> is <status> for <name>.` and a follow-up that uses the pipeline id. Exit code `0` means only that the run was accepted. With `--wait`, it prints `Run <id> succeeded.` or `Run <id> failed.` and sets the exit code accordingly; see [Exit codes](#exit-codes).
352
353
 
354
+ The platform rejects a run until the built-in lints of every node on the path from Start to End have finished. Lints start a few seconds after every `apply`, but not when dataset items or their values change, and a lint that runs for more than 5 minutes or waits more than 15 minutes to start fails as timed out. `pipeline lint` starts every lint without a current result: one that has not run on the current graph and dataset items, or whose last run failed. It always runs the lints that check dataset contents again, because edits to item values do not invalidate their results. It leaves running lints and the other finished lints alone and prints one line per lint. With `--wait` it waits for them, 5 minutes by default; exit code `0` means every lint finished, `1` that one failed or could not start, and `2` that one was still running when the wait ended. Read the findings with `toloka validate`.
355
+
356
+ `pipeline run` does the same before it starts a run, then waits only for the lints of nodes on the path from Start to End, up to 5 minutes or the `--timeout` you pass, which bounds the whole command including the wait for the run. When a lint is still running when that time is up, it prints the lints, starts nothing, and exits `2`. Otherwise it asks the platform to start the run, and the platform rejects it if a lint on a connected node failed.
357
+
353
358
  A run needs at least one dataset item that matches the root node's dataset and filter. Otherwise `pipeline run` fails with `No dataset items match, so a run would have nothing to process.` and starts nothing. `apply` and `validate` still succeed over an empty dataset, so upload items or widen the filter, then run again.
354
359
 
355
360
  `pipeline status` reports the latest run of that pipeline. `pipeline runs` lists recent runs (default 10). `pipeline cost` forecasts what a run would bill. `pipeline cancel` and `pipeline results` take a run id.
@@ -508,11 +513,13 @@ The catalog is bound to the installed CLI version, so a skill that a newer relea
508
513
 
509
514
  `toloka pipeline run --wait` maps the run outcome to the process exit code.
510
515
 
511
- | Code | `pipeline run --wait` | `check` / apply local gate |
512
- | ---- | ----------------------------------------------- | ------------------------------------------------- |
513
- | 0 | Run succeeded, or was accepted without `--wait` | All required local checks passed |
514
- | 1 | Run failed | Source, types, tests, or required coverage failed |
515
- | 2 | Run remained active when the wait window closed | A required checker could not run |
516
+ | Code | `pipeline run --wait` | `check` / apply local gate |
517
+ | ---- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
518
+ | 0 | Run succeeded, or was accepted without `--wait` | All required local checks passed |
519
+ | 1 | Run failed, or the platform refused to start it | Source, types, tests, or required coverage failed |
520
+ | 2 | Run remained active when the wait window closed, or lints were still running and no run was started | A required checker could not run |
521
+
522
+ The output tells the two exit-`2` cases apart: `Did not start a run of <name>: its lints are still running` means no run exists yet, so run `pipeline run` again later instead of polling `pipeline status`.
516
523
 
517
524
  Every other failure exits non-zero with a message on stderr: missing project identity, a rejected login, an unauthenticated `auth-status`, or a failed `secret set`.
518
525
 
@@ -79,7 +79,11 @@ If it lists no new run, retry. The same call has succeeded minutes later with no
79
79
 
80
80
  ## Exit code 2 from toloka pipeline run
81
81
 
82
- The timeout closed while the run was still in progress. That is not a failure of the run and not a failure of your pipeline. A queue with no one working it stays in progress indefinitely, so report what you observed — item counts and current states — rather than waiting unbounded:
82
+ Read the output first: exit `2` has two causes.
83
+
84
+ If it says `Did not start a run of <name>: its lints are still running`, no run was created. The platform rejects a run until the lints of the nodes on the path from Start to End have finished. Do not look for a run with `toloka pipeline status`: wait for the lints with `toloka pipeline lint <name> --wait`, then run `toloka pipeline run` again.
85
+
86
+ Otherwise the timeout closed while the run was still in progress. That is not a failure of the run and not a failure of your pipeline. A queue with no one working it stays in progress indefinitely, so report what you observed — item counts and current states — rather than waiting unbounded:
83
87
 
84
88
  ```sh
85
89
  toloka pipeline status <name>
@@ -2323,6 +2323,18 @@ export interface RunItem {
2323
2323
  state: RunItemState;
2324
2324
  }
2325
2325
 
2326
+ export type RunLintsResultRejectedItem = {
2327
+ nodeId: string;
2328
+ checkId: string;
2329
+ message: string;
2330
+ };
2331
+
2332
+ export interface RunLintsResult {
2333
+ checks: CheckJob[];
2334
+ rejected: RunLintsResultRejectedItem[];
2335
+ nodesInUse: string[];
2336
+ }
2337
+
2326
2338
  export type BatchInferenceModel = {
2327
2339
  type: 'base_model';
2328
2340
  baseModelId: 'qwen3-4b' | 'qwen3-8b' | 'qwen3-32b' | 'qwen3-235b';
@@ -102,6 +102,7 @@ import type {
102
102
  RunItem,
103
103
  RunItemId,
104
104
  RunJob,
105
+ RunLintsResult,
105
106
  SampleDatasetItemsBody,
106
107
  SampleJob,
107
108
  SampleResult,
@@ -2521,6 +2522,27 @@ return potatoFetch<CheckJob>(getCreatePipelineCheckUrl(pipelineId),
2521
2522
 
2522
2523
 
2523
2524
 
2525
+ export const getRunPipelineLintsUrl = (pipelineId: PipelineId,) => {
2526
+
2527
+
2528
+
2529
+
2530
+ return `/api/v2-beta/pipelines/${pipelineId}/lints`
2531
+ }
2532
+
2533
+ export const runPipelineLints = async (pipelineId: PipelineId, options?: Parameters<typeof potatoFetch>[1]): Promise<RunLintsResult> => {
2534
+
2535
+ return potatoFetch<RunLintsResult>(getRunPipelineLintsUrl(pipelineId),
2536
+ {
2537
+ ...options,
2538
+ method: 'POST'
2539
+
2540
+
2541
+ }
2542
+ );}
2543
+
2544
+
2545
+
2524
2546
  export const getGetPipelineCheckUrl = (checkRunId: CheckRunId,) => {
2525
2547
 
2526
2548
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toloka-platform/sdk",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "private": false,
5
5
  "description": "Build, inspect, and operate Toloka pipeline projects as TypeScript source. The public API may change between minor versions before 1.0.",
6
6
  "license": "Apache-2.0",
@@ -77,6 +77,6 @@
77
77
  "bun": ">=1.4.0"
78
78
  },
79
79
  "tolokaBuild": {
80
- "commit": "292c25ce81e0c8520df4f62a4c38a57d11f09300"
80
+ "commit": "1b8cbf8c1fd6ce6a32b5c8de98a7772fdcc3afe3"
81
81
  }
82
82
  }
@@ -11,8 +11,8 @@
11
11
  "testing",
12
12
  "checks"
13
13
  ],
14
- "version": "0.15.0",
15
- "digest": "48fd2f90e86b13fa0e57e4148ffc000dce1e7e2dcd88cac5d2a5128fd79c7ae6",
14
+ "version": "0.16.0",
15
+ "digest": "d029c12c76c569ad6ed9f12713890344e82c35445ba0f310d3aa2adf6f0610a5",
16
16
  "files": [
17
17
  "SKILL.md",
18
18
  "references/human-work.md",
@@ -33,7 +33,7 @@
33
33
  "bug",
34
34
  "report"
35
35
  ],
36
- "version": "0.15.0",
36
+ "version": "0.16.0",
37
37
  "digest": "70f0bd2c80b32f87896aeb27c363b8a2d37f40e22424f57fe1aad142e125ad67",
38
38
  "files": [
39
39
  "SKILL.md",
@@ -50,7 +50,7 @@
50
50
  "getting-started",
51
51
  "agent"
52
52
  ],
53
- "version": "0.15.0",
53
+ "version": "0.16.0",
54
54
  "digest": "2278adeb46a792356579395fcf73c42322cb23bcd0c61e20856ccabdad6762d1",
55
55
  "files": [
56
56
  "SKILL.md"
@@ -67,7 +67,7 @@
67
67
  "sft",
68
68
  "models"
69
69
  ],
70
- "version": "0.15.0",
70
+ "version": "0.16.0",
71
71
  "digest": "3ce10dee5d3c283d8b5fdea15134a58fc738c7af2c1bb19eb5a0bf70ea0bca13",
72
72
  "files": [
73
73
  "SKILL.md"
@@ -84,7 +84,7 @@
84
84
  "prompt",
85
85
  "models"
86
86
  ],
87
- "version": "0.15.0",
87
+ "version": "0.16.0",
88
88
  "digest": "be8bce8d9cf7241ccb60d2b2a9b7cc07d7755a76e8ff8ec1b0847c49efbb3469",
89
89
  "files": [
90
90
  "SKILL.md"
@@ -87,12 +87,12 @@ Let constructors mint IDs for new declarations. Preserve `id` options generated
87
87
  | Status | Meaning |
88
88
  | --- | --- |
89
89
  | `valid` | No blocking error. Warnings are informational. |
90
- | `checks-pending` | The only remaining issues are stored test cases or lints that have not run on this graph (`not_ran_node_check`). The user runs them from the node's Checks tab; they do not replace a passing local gate. |
90
+ | `checks-pending` | The only remaining issues are stored test cases or lints that have not run on this graph (`not_ran_node_check`). Run the lints with `toloka pipeline lint <name> --wait`; the user runs stored test cases from the node's Checks tab. Neither replaces a passing local gate. |
91
91
  | `invalid` | A source/spec error, a failed check (`failed_node_check`), or a lint error. Fix source/spec errors before plan or apply. Failed checks and lint errors do not block apply, so you can apply a fix for them and validate again, but a run cannot start until they are gone. |
92
92
  | `checks-running` | Only with `--wait`: a lint was still running when the wait ended. |
93
93
  | `skipped` | The pipeline or its dataset is not on the platform yet; `toloka apply` creates it. |
94
94
 
95
- The platform's lints, such as the Task UI interface lint and the quality-criteria lint, start by themselves a few seconds after every apply and take about a minute. Until they finish, plain `toloka validate` lists each as a `running_node_check` warning and can still print `valid` with exit 0, so it does not prove the lints passed. After applying a change, run `toloka validate --wait`: it validates again every 5 seconds, for up to 5 minutes, until no lint is running, and does not wait for a pipeline whose source has unapplied changes. Report the pipeline as valid only after that. When a reviewed plan adds a dataset field already bound by an existing live pipeline, pre-apply validation can report that exact field as missing; apply only the reviewed additive plan, then require post-apply validation to clear source/spec errors. Never rewrite valid source because a stored test case on the Checks tab has not run.
95
+ The platform's lints, such as the Task UI interface lint and the quality-criteria lint, start by themselves a few seconds after every apply and take about a minute. Until they finish, plain `toloka validate` lists each as a `running_node_check` warning and can still print `valid` with exit 0, so it does not prove the lints passed. After applying a change, run `toloka validate --wait`: it validates again every 5 seconds, for up to 5 minutes, until no lint is running, and does not wait for a pipeline whose source has unapplied changes. Report the pipeline as valid only after that. A lint does not restart by itself when dataset items or their values change, or after it fails or times out (after 5 minutes running or 15 minutes waiting to start): run `toloka pipeline lint <name> --wait`, which starts every lint without a current result and waits for it, then validate again. When a reviewed plan adds a dataset field already bound by an existing live pipeline, pre-apply validation can report that exact field as missing; apply only the reviewed additive plan, then require post-apply validation to clear source/spec errors. Never rewrite valid source because a stored test case on the Checks tab has not run.
96
96
 
97
97
  Register every code-node behavior with `test.code(pipeline, node, name, callback)`; its callback receives a bound `run(inputs, options?)` helper with the node's Python execution contract, and the test counts only after `run` executes, an assertion runs, and the callback passes. Register required human-work behavior with `test.ui(pipeline, node, name, callback)`; its callback receives a bound `render(options?)`, and the test counts only after a successful `submit`, at least one assertion, and a passing callback. Add at least one `test.pipeline(pipeline, name, callback)` scenario: treat the fixture as `root.started` item state, execute connected nodes in order with the callback's bound `runCode` and `renderUi` helpers, merge only each node's declared outputs, and assert the final `root.completed` fields. Cover meaningful match branches, optional paths, and failures as well as the happy path.
98
98
 
@@ -109,11 +109,11 @@ When plan, apply, or validate fails, follow [Recover safely](references/workflow
109
109
 
110
110
  ## Run an applied pipeline
111
111
 
112
- Before starting a run, require a passing `toloka check` and no blocking error from `toloka validate`. A `not_ran_node_check` or `running_node_check` warning does not block a run. Run `toloka pipeline cost <name>` and present the forecast for user approval before starting the run. If it skips a billed node, report the authored rate and a labeled labor floor instead of presenting `$0.00` as the price; make the missing pricing evidence clear when seeking approval.
112
+ Before starting a run, require a passing `toloka check` and no blocking error from `toloka validate`. A run cannot start until every lint of the pipeline has finished: `toloka pipeline run` first starts the lints without a current result and waits for them, up to 5 minutes or its `--timeout`, which bounds the whole command. It exits `2` without starting the run when a lint is still running at the end of that wait, and the platform rejects the run when a lint on a connected node failed. Run `toloka pipeline cost <name>` and present the forecast for user approval before starting the run. If it skips a billed node, report the authored rate and a labeled labor floor instead of presenting `$0.00` as the price; make the missing pricing evidence clear when seeking approval.
113
113
 
114
- Start a run with `toloka pipeline run <name> --wait --timeout <ms>`, then read its exit code: `0` succeeded, `1` reached a failed terminal state, `2` still running when the window closed. Without `--wait`, exit `0` only means the run was accepted. Nothing reports back later, so never describe a started run as a finished one. Name the live pipeline; there is no implicit only-pipeline.
114
+ Start a run with `toloka pipeline run <name> --wait --timeout <ms>`, then read its exit code: `0` succeeded, `1` reached a failed terminal state or the platform refused to start the run, `2` still running when the window closed. Exit `2` with `Did not start a run of <name>: its lints are still running` means no run was created: do not poll `toloka pipeline status` for it, run `toloka pipeline lint <name> --wait`, then start the run again. Without `--wait`, exit `0` only means the run was accepted. Nothing reports back later, so never describe a started run as a finished one. Name the live pipeline; there is no implicit only-pipeline.
115
115
 
116
- Keep monitoring bounded so the user receives a useful update even when expert work or queues take time. On exit `2`, report the observed item counts and states and say the run is still in progress; choose a further bounded check only when it serves the request. Use `toloka pipeline status <name>` and `toloka pipeline results <runId> --failed` for counts, states, and errors. `toloka status` (no prefix) is project health, not the latest run.
116
+ Keep monitoring bounded so the user receives a useful update even when expert work or queues take time. On exit `2` after the command printed a run id, report the observed item counts and states and say the run is still in progress; choose a further bounded check only when it serves the request. Use `toloka pipeline status <name>` and `toloka pipeline results <runId> --failed` for counts, states, and errors. `toloka status` (no prefix) is project health, not the latest run.
117
117
 
118
118
  ## Report with evidence
119
119
 
@@ -82,7 +82,9 @@ The platform generates `main_types.py` from the bound fields when it runs the no
82
82
 
83
83
  The platform runs code nodes on Python 3.12 with the standard library only, and a node cannot install packages. Call HTTP and LLM APIs with `urllib.request` or another standard module, not a vendor SDK. `test.code` runs your local Python, so an import of a package that happens to be installed locally passes `toloka check` and still fails on the platform.
84
84
 
85
- Bind every environment variable the code reads in `envVars`. Reference project secrets by name and set their values separately with `toloka secret set <name>`; use `os.environ['NAME']` in Python. The node's runtime has no ambient credentials.
85
+ Bind every environment variable the code reads in `envVars`. Reference project secrets by name and set their values separately with `toloka secret set <name>`; use `os.environ['NAME']` in Python. The node's runtime has no ambient credentials: a key that your own environment happens to hold, such as a coding agent's model credentials, is not available on the platform, so never use one in node code, test cases or scripts.
86
+
87
+ Claude Opus 4.7 and later and Claude Sonnet 5 and later reject sampling parameters: when a code node calls one of them, omit `temperature`, `top_p` and `top_k`. Claude 5.5 models also reject forced tool use, so do not set `tool_choice` to `any` or to a specific tool.
86
88
 
87
89
  ### Traps
88
90
 
@@ -70,6 +70,7 @@ Run the complete local gate first, then validate every bound pipeline remotely:
70
70
  toloka check
71
71
  toloka validate # before apply
72
72
  toloka validate --wait # after apply: waits for the platform's lints
73
+ toloka pipeline lint <name> --wait # after dataset changes or a failed lint: reruns the lints
73
74
  ```
74
75
 
75
76
  `toloka check` requires no credentials or live pipeline; SKILL.md describes what it runs and what its statuses mean. Use `toloka check <pipeline> --node <name-or-id>` for a focused edit loop, `--watch` for serialized reruns, and `--json` for automation.
@@ -8,6 +8,7 @@ import {
8
8
  listPipelineRuns,
9
9
  pipelineRunResults,
10
10
  pipelineRunStatus,
11
+ runPipelineLints,
11
12
  startPipelineRun,
12
13
  } from "../runs";
13
14
 
@@ -37,7 +38,11 @@ export const registerPipelineCommands = (program: Command, host: CommandHost): v
37
38
  .command("run")
38
39
  .argument("[name]", pipelineNameHelp)
39
40
  .option("--wait", "wait for the run to reach a terminal state", false)
40
- .option("--timeout <ms>", "how long to wait", positiveInteger)
41
+ .option(
42
+ "--timeout <ms>",
43
+ "how long the command may wait in total, for the lints and, with --wait, for the run",
44
+ positiveInteger,
45
+ )
41
46
  .description("start a run on an applied pipeline")
42
47
  .action(async (name: string | undefined, options: { wait: boolean; timeout?: number }) => {
43
48
  const { client, state, projectId } = await resolveContext(globals(), context);
@@ -53,6 +58,26 @@ export const registerPipelineCommands = (program: Command, host: CommandHost): v
53
58
  context.io.stdout(result.output);
54
59
  exitCode.value = exitCodeForOutcome(result.outcome);
55
60
  });
61
+ pipeline
62
+ .command("lint")
63
+ .argument("[name]", pipelineNameHelp)
64
+ .option("--wait", "wait for the lints to finish", false)
65
+ .option("--timeout <ms>", "how long to wait", positiveInteger)
66
+ .description("run the pipeline's lints that have no current result")
67
+ .action(async (name: string | undefined, options: { wait: boolean; timeout?: number }) => {
68
+ const { client, state, projectId } = await resolveContext(globals(), context);
69
+ assertProjectSelected(projectId);
70
+ const result = await runPipelineLints(client, {
71
+ projectId,
72
+ state,
73
+ ...(name === undefined ? {} : { pipeline: name }),
74
+ wait: options.wait,
75
+ ...(options.timeout === undefined ? {} : { timeoutMs: options.timeout }),
76
+ ...(context.io.select === undefined ? {} : { select: context.io.select }),
77
+ });
78
+ context.io.stdout(result.output);
79
+ exitCode.value = exitCodeForOutcome(result.outcome);
80
+ });
56
81
  pipeline
57
82
  .command("status")
58
83
  .argument("[name]", pipelineNameHelp)
package/src/cli/runs.ts CHANGED
@@ -4,6 +4,7 @@ import { PotatoApiError } from "../errors";
4
4
  import type { TolokaState } from "../state";
5
5
  import type { RunItem, RunJob } from "../index";
6
6
  import { assertCli } from "./cli-error";
7
+ import { apiErrorDetails } from "./format-error";
7
8
 
8
9
  export type PipelineRef = {
9
10
  readonly pipelineId: string;
@@ -249,6 +250,144 @@ type PipelineCommandOptions = {
249
250
  readonly select?: (choices: readonly { name: string; value: string }[]) => Promise<string>;
250
251
  };
251
252
 
253
+ const LINT_WAIT_TIMEOUT_MS = 5 * 60_000;
254
+ const LINT_POLL_INTERVAL_MS = 5_000;
255
+
256
+ type LintJob = Awaited<ReturnType<PotatoClient["checks"]["runLints"]>>["checks"][number];
257
+
258
+ const isLintFinished = (job: LintJob) => job.status === "succeeded" || job.status === "failed";
259
+
260
+ const waitForLints = async (
261
+ client: PotatoClient,
262
+ jobs: readonly LintJob[],
263
+ timeoutMs: number,
264
+ intervalMs: number,
265
+ ): Promise<readonly LintJob[]> => {
266
+ const deadline = Date.now() + timeoutMs;
267
+ let current = jobs;
268
+ while (!current.every(isLintFinished) && Date.now() < deadline) {
269
+ await new Promise<void>((resolve) => {
270
+ setTimeout(resolve, Math.min(intervalMs, deadline - Date.now()));
271
+ });
272
+ current = await Promise.all(
273
+ current.map(async (job) => (isLintFinished(job) ? job : client.checks.get({ checkRunId: job.id }))),
274
+ );
275
+ }
276
+ return current;
277
+ };
278
+
279
+ const runLints = async (
280
+ client: PotatoClient,
281
+ pipelineId: string,
282
+ options: {
283
+ readonly wait: boolean;
284
+ readonly nodesInUseOnly: boolean;
285
+ readonly timeoutMs?: number;
286
+ readonly intervalMs?: number;
287
+ },
288
+ ): Promise<{ readonly outcome: RunOutcome; readonly lines: readonly string[] }> => {
289
+ const result = await client.checks.runLints({ pipelineId });
290
+ const inScope = (nodeId: string) => !options.nodesInUseOnly || result.nodesInUse.includes(nodeId);
291
+ const checks = result.checks.filter((job) => inScope(job.nodeId));
292
+ const rejected = result.rejected.filter((entry) => inScope(entry.nodeId));
293
+ const jobs = options.wait
294
+ ? await waitForLints(
295
+ client,
296
+ checks,
297
+ options.timeoutMs ?? LINT_WAIT_TIMEOUT_MS,
298
+ options.intervalMs ?? LINT_POLL_INTERVAL_MS,
299
+ )
300
+ : checks;
301
+ const outcome: RunOutcome =
302
+ options.wait && !jobs.every(isLintFinished)
303
+ ? "timedOut"
304
+ : rejected.length > 0 || jobs.some((job) => job.status === "failed")
305
+ ? "failed"
306
+ : jobs.every(isLintFinished)
307
+ ? "succeeded"
308
+ : "started";
309
+ return {
310
+ outcome,
311
+ lines: [
312
+ ...jobs.map(
313
+ (job) =>
314
+ ` ${job.status} ${job.nodeId}/${job.checkId}${job.error === undefined || job.error === null ? "" : `: ${job.error}`}`,
315
+ ),
316
+ ...rejected.map((entry) => ` rejected ${entry.nodeId}/${entry.checkId}: ${entry.message}`),
317
+ ],
318
+ };
319
+ };
320
+
321
+ export async function runPipelineLints(
322
+ client: PotatoClient,
323
+ options: PipelineCommandOptions & {
324
+ readonly wait?: boolean;
325
+ readonly timeoutMs?: number;
326
+ readonly intervalMs?: number;
327
+ },
328
+ ): Promise<RunCommandResult> {
329
+ const ref = await resolvePipelineRef({ client, ...options });
330
+ const { outcome, lines } = await runLints(client, ref.pipelineId, {
331
+ wait: options.wait === true,
332
+ nodesInUseOnly: false,
333
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
334
+ ...(options.intervalMs === undefined ? {} : { intervalMs: options.intervalMs }),
335
+ });
336
+ const summary =
337
+ outcome === "succeeded"
338
+ ? `All lints of ${ref.name} have finished. Run toloka validate to read their findings.`
339
+ : outcome === "failed"
340
+ ? `Some lints of ${ref.name} failed or could not start. Fix the reported problems, then run toloka pipeline lint again.`
341
+ : outcome === "timedOut"
342
+ ? `Some lints of ${ref.name} are still running. Run toloka pipeline lint --wait again.`
343
+ : `Started the lints of ${ref.name}. Follow them with toloka pipeline lint --wait.`;
344
+ return { outcome, output: [summary, ...lines].join("\n") };
345
+ }
346
+
347
+ const startRunAfterLints = async (
348
+ client: PotatoClient,
349
+ ref: PipelineRef,
350
+ options: { readonly timeoutMs?: number; readonly intervalMs?: number },
351
+ ): Promise<{ readonly run: RunJob } | { readonly result: RunCommandResult }> => {
352
+ const lints = await runLints(client, ref.pipelineId, {
353
+ wait: true,
354
+ nodesInUseOnly: true,
355
+ ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
356
+ ...(options.intervalMs === undefined ? {} : { intervalMs: options.intervalMs }),
357
+ }).catch((error: unknown) => {
358
+ if (error instanceof PotatoApiError && error.status === 403) return undefined;
359
+ throw error;
360
+ });
361
+
362
+ if (lints?.outcome === "timedOut") {
363
+ return {
364
+ result: {
365
+ outcome: "timedOut",
366
+ output: [
367
+ `Did not start a run of ${ref.name}: its lints are still running. Run toloka pipeline run again once toloka pipeline lint --wait reports them finished.`,
368
+ ...lints.lines,
369
+ ].join("\n"),
370
+ },
371
+ };
372
+ }
373
+
374
+ try {
375
+ return { run: await client.runs.start({ pipelineId: ref.pipelineId }) };
376
+ } catch (error) {
377
+ if (lints?.outcome !== "failed" || !(error instanceof PotatoApiError) || error.status === undefined) throw error;
378
+ return {
379
+ result: {
380
+ outcome: "failed",
381
+ output: [
382
+ `Could not start a run of ${ref.name}: ${apiErrorDetails(error) ?? error.message}`,
383
+ "Its lints:",
384
+ ...lints.lines,
385
+ ].join("\n"),
386
+ },
387
+ };
388
+ }
389
+ };
390
+
252
391
  export async function startPipelineRun(
253
392
  client: PotatoClient,
254
393
  options: PipelineCommandOptions & {
@@ -258,7 +397,12 @@ export async function startPipelineRun(
258
397
  },
259
398
  ): Promise<RunCommandResult> {
260
399
  const ref = await resolvePipelineRef({ client, ...options });
261
- const started = await client.runs.start({ pipelineId: ref.pipelineId });
400
+ const startedAt = Date.now();
401
+ const preparation = await startRunAfterLints(client, ref, options);
402
+
403
+ if ("result" in preparation) return preparation.result;
404
+
405
+ const started = preparation.run;
262
406
 
263
407
  if (options.wait !== true) {
264
408
  return {
@@ -274,7 +418,9 @@ export async function startPipelineRun(
274
418
  const finished = await client.waitFor(
275
419
  { type: "pipelineRun", runId: started.id },
276
420
  {
277
- ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
421
+ ...(options.timeoutMs === undefined
422
+ ? {}
423
+ : { timeoutMs: Math.max(0, options.timeoutMs - (Date.now() - startedAt)) }),
278
424
  ...(options.intervalMs === undefined ? {} : { intervalMs: options.intervalMs }),
279
425
  },
280
426
  );
package/src/client.ts CHANGED
@@ -790,6 +790,8 @@ const createClientImplementation = (config: ClientConfig) => {
790
790
  wire.createPipelineCheck(pipelineId, body, requestOptions),
791
791
  get: ({ checkRunId }: { checkRunId: CheckRunId }) =>
792
792
  wire.getPipelineCheck(checkRunId, requestOptions),
793
+ runLints: ({ pipelineId }: { pipelineId: PipelineId }) =>
794
+ wire.runPipelineLints(pipelineId, requestOptions),
793
795
  };
794
796
 
795
797
  const deliveryOrders = {
@@ -1478,7 +1478,8 @@
1478
1478
  },
1479
1479
  "required": [
1480
1480
  "qaMode"
1481
- ]
1481
+ ],
1482
+ "additionalProperties": false
1482
1483
  },
1483
1484
  {
1484
1485
  "type": "object",
@@ -1513,7 +1514,8 @@
1513
1514
  },
1514
1515
  "required": [
1515
1516
  "qaMode"
1516
- ]
1517
+ ],
1518
+ "additionalProperties": false
1517
1519
  }
1518
1520
  ]
1519
1521
  },