@toist/aja 0.6.1 → 0.8.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.
package/src/db-handles.ts DELETED
@@ -1,70 +0,0 @@
1
- // 2121
2
- // Singleton db handle holder + lifecycle orchestration. Decouples the db
3
- // modules (which are now pure factories per `openX()`) from the consumers
4
- // (index.ts, pipeline.ts) that need stable handle references throughout the
5
- // HTTP request lifetime.
6
- //
7
- // Lifecycle (matches context/instance-spec.md §7):
8
- //
9
- // initDbs() — acquire runner lock, migrate runtime.db, open all three
10
- // handles. Idempotent: second call is a no-op when handles
11
- // are already populated.
12
- // runtimeDb() — getter; throws when init has not run
13
- // dataDb() — getter; throws when init has not run
14
- // cacheDb() — getter; throws when init has not run
15
- // closeDbs() — close all three handles, release the lock. Used by
16
- // `startRunner`'s graceful stop path.
17
- //
18
- // Why getters and not exported instances: the db modules used to do their
19
- // init at module load via top-level await, which forced lifecycle to happen
20
- // during the import graph. That works for source-mod single-instance runs
21
- // but breaks the moment startRunner() needs to set rootDir before init runs.
22
- // Getters defer the resolution to call time; init runs once, before any
23
- // HTTP request fires.
24
-
25
- import type { Database } from "bun:sqlite"
26
- import { openRuntimeDb } from "./runtime-db.ts"
27
- import { openDataDb } from "./data-db.ts"
28
- import { openCacheDb } from "./cache-db.ts"
29
- import { acquireRunnerLock, releaseRunnerLock } from "./lock.ts"
30
-
31
- let _runtimeDb: Database | null = null
32
- let _dataDb: Database | null = null
33
- let _cacheDb: Database | null = null
34
-
35
- export async function initDbs(): Promise<void> {
36
- if (_runtimeDb !== null) return // idempotent
37
-
38
- await acquireRunnerLock()
39
- _runtimeDb = openRuntimeDb()
40
- _dataDb = openDataDb()
41
- _cacheDb = openCacheDb()
42
- }
43
-
44
- export async function closeDbs(): Promise<void> {
45
- if (_runtimeDb) { _runtimeDb.close(); _runtimeDb = null }
46
- if (_dataDb) { _dataDb.close(); _dataDb = null }
47
- if (_cacheDb) { _cacheDb.close(); _cacheDb = null }
48
- await releaseRunnerLock()
49
- }
50
-
51
- export function runtimeDb(): Database {
52
- if (_runtimeDb === null) {
53
- throw new Error("[db-handles] runtimeDb() called before initDbs() — lifecycle violation")
54
- }
55
- return _runtimeDb
56
- }
57
-
58
- export function dataDb(): Database {
59
- if (_dataDb === null) {
60
- throw new Error("[db-handles] dataDb() called before initDbs() — lifecycle violation")
61
- }
62
- return _dataDb
63
- }
64
-
65
- export function cacheDb(): Database {
66
- if (_cacheDb === null) {
67
- throw new Error("[db-handles] cacheDb() called before initDbs() — lifecycle violation")
68
- }
69
- return _cacheDb
70
- }
package/src/hitl.ts DELETED
@@ -1,257 +0,0 @@
1
- // 2121
2
- // Platform-side HITL (human-in-the-loop) primitives.
3
- //
4
- // `human.input` kinds call ctx.suspend(spec). The suspend implementation
5
- // here decides between three cases:
6
- //
7
- // 1. An answered task already exists for this node in this run
8
- // → return its response, kind continues normally.
9
- // 2. An open task exists (re-suspension before resume completed)
10
- // → throw HitlSuspend referencing that task.
11
- // 3. No task yet
12
- // → create a new task with a single-use response_token, throw HitlSuspend.
13
- //
14
- // runPipeline catches HitlSuspend and converts it into a SUSPENDED outcome.
15
- // The HTTP layer persists node_outputs, marks runs.status='suspended', and
16
- // returns the task descriptor to the caller.
17
- //
18
- // On resume, the HTTP layer hydrates node_outputs back into the executor's
19
- // results map. Re-execution from the suspended node calls ctx.suspend again;
20
- // case (1) above kicks in and the human.input node returns the response.
21
- // Side effects in already-completed nodes do not re-run because the executor
22
- // skips any node whose output is in the resume map.
23
-
24
- import { randomBytes } from "node:crypto"
25
- import type { Database } from "bun:sqlite"
26
- import type { HitlSpec, ErrorReviewSpec } from "@toist/spec"
27
-
28
- export type { HitlSpec, ErrorReviewSpec }
29
-
30
- export interface TaskDescriptor {
31
- id: number
32
- runId: number
33
- pipeline: string | null
34
- nodeId: string
35
- /** "human.input" for HITL pauses, "error_review" for failed-node suspends. */
36
- kind: string
37
- prompt: string
38
- /** For human.input: the structured schema for the response form.
39
- * For error_review: the structured error details (kind id, name) to display. */
40
- schema: unknown
41
- assignee: string | null
42
- responseToken: string
43
- status: "open" | "answered" | "expired" | "cancelled"
44
- }
45
-
46
- /**
47
- * Thrown by ctx.suspend() to signal "this run cannot proceed past this node
48
- * without external input". runPipeline catches this and converts it to a
49
- * SUSPENDED RunOutcome — it does not propagate to the HTTP layer as an error.
50
- */
51
- export class HitlSuspend extends Error {
52
- constructor(
53
- public readonly taskId: number,
54
- public readonly nodeId: string,
55
- public readonly spec: HitlSpec,
56
- ) {
57
- super(`HITL suspend at node "${nodeId}" (task ${taskId})`)
58
- this.name = "HitlSuspend"
59
- }
60
- }
61
-
62
- /**
63
- * Thrown by the dispatcher when a node fails and its onError policy is
64
- * "suspend". Mirrors HitlSuspend's shape so the dispatcher can treat both
65
- * via a single suspend path. The persisted task carries kind='error_review';
66
- * resume payload is `{ action: "retry" | "skip" | "abort", value? }`.
67
- */
68
- export class ErrorReviewSuspend extends Error {
69
- constructor(
70
- public readonly taskId: number,
71
- public readonly nodeId: string,
72
- public readonly spec: ErrorReviewSpec,
73
- ) {
74
- super(`error_review suspend at node "${nodeId}" (task ${taskId})`)
75
- this.name = "ErrorReviewSuspend"
76
- }
77
- }
78
-
79
- /**
80
- * Build a suspend(spec) function bound to a specific run + node. Called
81
- * fresh for each step in the executor; the closure captures runId / nodeId
82
- * so the kind itself never has to think about identifiers.
83
- */
84
- export function makeSuspend(
85
- runtimeDb: Database,
86
- runId: number,
87
- nodeId: string,
88
- ): (spec: HitlSpec) => Promise<unknown> {
89
- return async (spec: HitlSpec): Promise<unknown> => {
90
- // Case 1: already answered — return the response, kind continues.
91
- const answered = runtimeDb.prepare(
92
- "SELECT response_json FROM tasks WHERE run_id = ? AND node_id = ? AND status = 'answered' ORDER BY id DESC LIMIT 1",
93
- ).get(runId, nodeId) as { response_json: string | null } | undefined
94
-
95
- if (answered) {
96
- return answered.response_json ? JSON.parse(answered.response_json) : null
97
- }
98
-
99
- // Case 2: an open task already exists — re-throw without creating a duplicate.
100
- const open = runtimeDb.prepare(
101
- "SELECT id FROM tasks WHERE run_id = ? AND node_id = ? AND status = 'open' ORDER BY id DESC LIMIT 1",
102
- ).get(runId, nodeId) as { id: number } | undefined
103
-
104
- if (open) {
105
- throw new HitlSuspend(open.id, nodeId, spec)
106
- }
107
-
108
- // Case 3: create new task with a single-use response token.
109
- const token = randomBytes(16).toString("hex")
110
- const inserted = runtimeDb.prepare(
111
- `INSERT INTO tasks (run_id, node_id, kind, prompt, schema_json, assignee, response_token)
112
- VALUES (?, ?, 'human.input', ?, ?, ?, ?)
113
- RETURNING id`,
114
- ).get(
115
- runId, nodeId, spec.prompt,
116
- spec.schema !== undefined ? JSON.stringify(spec.schema) : null,
117
- spec.assignee ?? null,
118
- token,
119
- ) as { id: number }
120
-
121
- throw new HitlSuspend(inserted.id, nodeId, spec)
122
- }
123
- }
124
-
125
- /**
126
- * Create a fresh `error_review` task for a failing node and return its
127
- * id + token. Called by the dispatcher when a node throws and its onError
128
- * policy is "suspend". Same pattern as makeSuspend's case-3 path: insert
129
- * with a single-use response_token, then throw ErrorReviewSuspend so the
130
- * dispatcher's suspend path takes over.
131
- *
132
- * The task's `prompt` carries the error message; structured details (kind,
133
- * stack) live in `schema_json` so the UI can surface them without an
134
- * additional column. Resume payload distinguishes by tasks.kind, not by
135
- * column shape — keeps the existing tasks table unchanged.
136
- */
137
- export function createErrorReviewTask(
138
- runtimeDb: Database,
139
- runId: number,
140
- nodeId: string,
141
- spec: ErrorReviewSpec,
142
- ): { id: number; token: string } {
143
- const token = randomBytes(16).toString("hex")
144
- const inserted = runtimeDb.prepare(
145
- `INSERT INTO tasks (run_id, node_id, kind, prompt, schema_json, assignee, response_token)
146
- VALUES (?, ?, 'error_review', ?, ?, ?, ?)
147
- RETURNING id`,
148
- ).get(
149
- runId, nodeId, spec.prompt,
150
- spec.details !== undefined ? JSON.stringify(spec.details) : null,
151
- spec.assignee ?? null,
152
- token,
153
- ) as { id: number }
154
- return { id: inserted.id, token }
155
- }
156
-
157
- /**
158
- * Persist node outputs gathered before a suspension. Idempotent: re-running
159
- * the same insertion is a no-op via INSERT OR REPLACE — useful when the
160
- * executor re-runs upstream nodes during a resume.
161
- */
162
- export function persistNodeOutputs(
163
- runtimeDb: Database,
164
- runId: number,
165
- outputs: Record<string, unknown>,
166
- ): void {
167
- const stmt = runtimeDb.prepare(
168
- `INSERT OR REPLACE INTO node_outputs (run_id, node_id, output_json, finished_at)
169
- VALUES (?, ?, ?, datetime('now'))`,
170
- )
171
- const tx = runtimeDb.transaction(() => {
172
- for (const [nodeId, output] of Object.entries(outputs)) {
173
- stmt.run(runId, nodeId, JSON.stringify(output ?? null))
174
- }
175
- })
176
- tx()
177
- }
178
-
179
- /**
180
- * Load all previously-persisted node outputs for a run. Returned as a
181
- * { nodeId → output } map ready to seed the executor's results scope.
182
- */
183
- export function loadNodeOutputs(
184
- runtimeDb: Database,
185
- runId: number,
186
- ): Record<string, unknown> {
187
- const rows = runtimeDb.prepare(
188
- "SELECT node_id, output_json FROM node_outputs WHERE run_id = ?",
189
- ).all(runId) as { node_id: string; output_json: string | null }[]
190
-
191
- const out: Record<string, unknown> = {}
192
- for (const row of rows) {
193
- out[row.node_id] = row.output_json ? JSON.parse(row.output_json) : null
194
- }
195
- return out
196
- }
197
-
198
- /**
199
- * Fetch a single task by id. Joins runs to surface the pipeline id alongside
200
- * the task — useful for cross-section navigation in the UI. Returns null if
201
- * not found.
202
- */
203
- export function getTask(runtimeDb: Database, taskId: number): TaskDescriptor | null {
204
- const row = runtimeDb.prepare(
205
- `SELECT t.id, t.run_id, t.node_id, t.kind, t.prompt, t.schema_json, t.assignee,
206
- t.response_token, t.status, r.pipeline AS pipeline
207
- FROM tasks t LEFT JOIN runs r ON t.run_id = r.id
208
- WHERE t.id = ?`,
209
- ).get(taskId) as {
210
- id: number; run_id: number; node_id: string; kind: string; prompt: string | null
211
- schema_json: string | null; assignee: string | null
212
- response_token: string; status: string; pipeline: string | null
213
- } | undefined
214
-
215
- if (!row) return null
216
- return {
217
- id: row.id,
218
- runId: row.run_id,
219
- pipeline: row.pipeline,
220
- nodeId: row.node_id,
221
- kind: row.kind,
222
- prompt: row.prompt ?? "",
223
- schema: row.schema_json ? JSON.parse(row.schema_json) : null,
224
- assignee: row.assignee,
225
- responseToken: row.response_token,
226
- status: row.status as TaskDescriptor["status"],
227
- }
228
- }
229
-
230
- /**
231
- * Record a response against an open task. Validates the token matches and
232
- * the task is still open; refuses replay (token is single-use). Returns the
233
- * updated task descriptor on success, throws on validation failure.
234
- */
235
- export function answerTask(
236
- runtimeDb: Database,
237
- taskId: number,
238
- token: string,
239
- response: unknown,
240
- respondedBy: string | null,
241
- ): TaskDescriptor {
242
- const task = getTask(runtimeDb, taskId)
243
- if (!task) throw new Error(`task ${taskId} not found`)
244
- if (task.status !== "open") throw new Error(`task ${taskId} is ${task.status}, cannot answer`)
245
- if (task.responseToken !== token) throw new Error(`task ${taskId}: invalid response token`)
246
-
247
- runtimeDb.prepare(
248
- `UPDATE tasks
249
- SET status = 'answered',
250
- response_json = ?,
251
- responded_by = ?,
252
- responded_at = datetime('now')
253
- WHERE id = ?`,
254
- ).run(JSON.stringify(response ?? null), respondedBy, taskId)
255
-
256
- return { ...task, status: "answered" }
257
- }
package/src/instance.ts DELETED
@@ -1,64 +0,0 @@
1
- // 2121
2
- // Per-instance metadata: what kind of platform-instance this is (which
3
- // customer, what tier, what teasers to surface), distinct from the runtime
4
- // state in runtime.db.
5
- //
6
- // Lives at <rootDir>/instance.json (per context/instance-spec.md §6).
7
- // Optional — when missing, sensible defaults are returned (useful for the
8
- // in-monorepo template-runner where no scaffold has happened).
9
- //
10
- // Schema (current, may grow):
11
- //
12
- // {
13
- // "platformVersion": "0.1.0", // recorded at scaffold time
14
- // "instanceName": "Enio Runner", // human-readable label, optional
15
- // "tier": "pilot", // free-form string, optional
16
- // "teasers": [ // available-but-not-shipped pipelines
17
- // {
18
- // "id": "sales-kpi",
19
- // "title": "Salesman KPI tracking",
20
- // "description": "Weekly KPI rollup per salesman with conversion tracking.",
21
- // "category": "analytics"
22
- // }
23
- // ]
24
- // }
25
-
26
- import { existsSync, readFileSync } from "node:fs"
27
- import { instanceFilePath } from "./config.ts"
28
-
29
- export interface Teaser {
30
- id: string
31
- title: string
32
- description?: string
33
- category?: string
34
- }
35
-
36
- export interface Instance {
37
- platformVersion?: string
38
- instanceName?: string
39
- tier?: string
40
- teasers: Teaser[]
41
- }
42
-
43
- const DEFAULTS: Instance = { teasers: [] }
44
-
45
- // Re-read on every call. instance.json is tiny and changes rarely; the cost
46
- // is negligible and avoids stale-cache surprises when the file is edited.
47
- export function loadInstance(): Instance {
48
- const path = instanceFilePath()
49
- if (!existsSync(path)) return DEFAULTS
50
-
51
- try {
52
- const raw = readFileSync(path, "utf8")
53
- const parsed = JSON.parse(raw) as Partial<Instance>
54
- return {
55
- platformVersion: parsed.platformVersion,
56
- instanceName: parsed.instanceName,
57
- tier: parsed.tier,
58
- teasers: Array.isArray(parsed.teasers) ? parsed.teasers : [],
59
- }
60
- } catch (err) {
61
- console.warn(`[instance] failed to read ${path}:`, (err as Error).message)
62
- return DEFAULTS
63
- }
64
- }
@@ -1,26 +0,0 @@
1
- // 2121
2
- import type { NodeKind } from "./types.ts"
3
-
4
- export const trigger: NodeKind<Record<string, never>, Record<string, never>> = {
5
- id: "trigger",
6
- category: "control",
7
- label: "Trigger",
8
- description: "Pipeline entrypoint. Outputs the run payload — reference the run params with $params.X anywhere in the graph.",
9
- icon: "Zap",
10
- params: {},
11
- inputs: {},
12
- outputs: { payload: { type: "any" } },
13
- run: (_ctx, _params, _input) => null,
14
- }
15
-
16
- export const sink: NodeKind<Record<string, never>, { value: unknown }> = {
17
- id: "sink",
18
- category: "control",
19
- label: "Output",
20
- description: "Pipeline output. The value passed in becomes the run result.",
21
- icon: "ArrowUpFromLine",
22
- params: {},
23
- inputs: { value: { type: "any", required: true } },
24
- outputs: {},
25
- run: (_ctx, _params, input) => input.value,
26
- }
package/src/kinds/data.ts DELETED
@@ -1,30 +0,0 @@
1
- // 2121
2
- import type { NodeKind } from "./types.ts"
3
-
4
- export const dataJson: NodeKind<{ value: unknown }, Record<string, never>> = {
5
- id: "data.json",
6
- category: "data",
7
- label: "JSON",
8
- description: "Emit a constant JSON value as the output. Useful as a source.",
9
- icon: "Braces",
10
- params: {
11
- value: { type: "json", label: "Value", required: true, description: "Any JSON value (object, array, scalar)" },
12
- },
13
- inputs: {},
14
- outputs: { value: { type: "any" } },
15
- run: (_ctx, params) => params.value,
16
- }
17
-
18
- export const dataMerge: NodeKind<{ overlay?: object }, { input: object }> = {
19
- id: "data.merge",
20
- category: "data",
21
- label: "Merge",
22
- description: "Shallow-merge runtime input with a constant overlay.",
23
- icon: "Combine",
24
- params: {
25
- overlay: { type: "json", label: "Overlay", default: {} },
26
- },
27
- inputs: { input: { type: "object", required: true } },
28
- outputs: { output: { type: "object" } },
29
- run: (_ctx, params, input) => ({ ...input.input, ...(params.overlay ?? {}) }),
30
- }
package/src/kinds/db.ts DELETED
@@ -1,92 +0,0 @@
1
- // 2121
2
- import type { NodeKind } from "./types.ts"
3
-
4
- export const dbInsert: NodeKind<
5
- { table: string; mode?: "insert" | "upsert"; key?: string | string[] },
6
- { rows: unknown[] }
7
- > = {
8
- id: "db.insert",
9
- category: "db",
10
- label: "DB Insert",
11
- description:
12
- "Insert (or upsert) array items into a SQLite table. Columns are inferred from the first row. " +
13
- "For mode=upsert you must specify `key` (column or column list) — a UNIQUE index is created on " +
14
- "first run so subsequent runs replace matching rows instead of duplicating them.",
15
- icon: "Database",
16
- sideEffect: true,
17
- params: {
18
- table: { type: "string", label: "Table", required: true, placeholder: "salesman_kpi" },
19
- mode: { type: "select", label: "Mode", default: "insert",
20
- options: [
21
- { value: "insert", label: "INSERT (append)" },
22
- { value: "upsert", label: "UPSERT (replace by key)" },
23
- ] },
24
- key: { type: "string", label: "Unique key", description: "Column name (or comma-separated list) for UPSERT mode. The kind creates a UNIQUE index on first run.", placeholder: "id" },
25
- },
26
- inputs: { rows: { type: "array", required: true } },
27
- outputs: { result: { type: "object" } },
28
- run: (ctx, params, input) => {
29
- const rows = Array.isArray(input.rows) ? input.rows : []
30
- if (rows.length === 0) return { inserted: 0, table: params.table }
31
-
32
- const cols = Object.keys(rows[0] as Record<string, unknown>)
33
- if (cols.length === 0) return { inserted: 0, table: params.table }
34
-
35
- const mode = params.mode ?? "insert"
36
-
37
- if (mode === "upsert" && !params.key) {
38
- throw new Error(
39
- `db.insert: mode "upsert" requires "key" param (column or column list). ` +
40
- `Without a UNIQUE index every run would duplicate every row.`,
41
- )
42
- }
43
-
44
- const colDefs = cols.map((c) => `${c} TEXT`).join(", ")
45
- ctx.db.exec(`CREATE TABLE IF NOT EXISTS ${params.table} (${colDefs})`)
46
-
47
- if (mode === "upsert") {
48
- const keyCols = Array.isArray(params.key) ? params.key : String(params.key).split(",").map(s => s.trim())
49
- const idxName = `idx_${params.table}_${keyCols.join("_")}_unique`
50
- try {
51
- ctx.db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS ${idxName} ON ${params.table} (${keyCols.join(", ")})`)
52
- } catch (err) {
53
- throw new Error(
54
- `db.insert: cannot create UNIQUE index on ${params.table}(${keyCols.join(", ")}) — ` +
55
- `existing rows likely violate uniqueness. Drop the table or remove duplicates first. ` +
56
- `Underlying error: ${(err as Error).message}`,
57
- )
58
- }
59
- }
60
-
61
- const placeholders = cols.map(() => "?").join(", ")
62
- const verb = mode === "upsert" ? "INSERT OR REPLACE" : "INSERT"
63
- const stmt = ctx.db.prepare(`${verb} INTO ${params.table} (${cols.join(", ")}) VALUES (${placeholders})`)
64
-
65
- const txn = ctx.db.transaction((items: unknown[]) => {
66
- for (const it of items) {
67
- const r = it as Record<string, unknown>
68
- stmt.run(...cols.map((c) => {
69
- const v = r[c]
70
- return typeof v === "object" && v !== null ? JSON.stringify(v) : (v as never)
71
- }))
72
- }
73
- })
74
- txn(rows)
75
-
76
- return { inserted: rows.length, table: params.table, mode }
77
- },
78
- }
79
-
80
- export const dbQuery: NodeKind<{ sql: string }, Record<string, never>> = {
81
- id: "db.query",
82
- category: "db",
83
- label: "DB Query",
84
- description: "Run a SELECT query against the runner's SQLite DB.",
85
- icon: "Search",
86
- params: {
87
- sql: { type: "string", label: "SQL", required: true, placeholder: "SELECT * FROM salesman_kpi ORDER BY score DESC LIMIT 10" },
88
- },
89
- inputs: {},
90
- outputs: { rows: { type: "array" } },
91
- run: (ctx, params) => ctx.db.prepare(params.sql).all(),
92
- }
package/src/kinds/hitl.ts DELETED
@@ -1,56 +0,0 @@
1
- // 2121
2
- // HITL kinds. The actual suspend/resume bookkeeping lives in ../hitl.ts on
3
- // the platform side; this file only declares the kind contract.
4
-
5
- import type { NodeKind } from "./types.ts"
6
-
7
- interface HumanInputParams {
8
- prompt: string
9
- assignee?: string
10
- schema?: unknown
11
- timeout?: string
12
- }
13
-
14
- export const humanInput: NodeKind<HumanInputParams, Record<string, never>> = {
15
- id: "human.input",
16
- category: "control",
17
- label: "Human input",
18
- description: "Suspends the run until the assignee responds with input. The response becomes this node's output and downstream nodes can $results.<id>.<field> it.",
19
- icon: "MessageSquareReply",
20
- params: {
21
- prompt: {
22
- type: "string", required: true,
23
- description: "Question shown to the assignee in the task queue.",
24
- placeholder: "Approve campaign launch?",
25
- },
26
- assignee: {
27
- type: "string",
28
- description: "Who should respond. Format: user:<email> or agent:<id>. Empty = open to anyone.",
29
- placeholder: "user:teemu@2121.fi",
30
- },
31
- schema: {
32
- type: "json",
33
- description: "JSON Schema describing the expected response shape. v1: descriptive only, not enforced.",
34
- },
35
- timeout: {
36
- type: "string",
37
- description: "ISO 8601 duration before the task expires (e.g. \"P7D\"). v1: stored, not enforced.",
38
- placeholder: "P7D",
39
- },
40
- },
41
- inputs: {},
42
- outputs: {
43
- response: {
44
- type: "any",
45
- description: "Whatever the assignee submitted as their response.",
46
- },
47
- },
48
- run: async (ctx, params) => {
49
- return await ctx.suspend({
50
- prompt: params.prompt,
51
- assignee: params.assignee,
52
- schema: params.schema,
53
- timeout: params.timeout,
54
- })
55
- },
56
- }