@deepseek-ai/dsh-workflow-worker-thread 0.0.1-rc.3

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/lib/worker.cjs ADDED
@@ -0,0 +1,783 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
15
+ }
16
+ return to;
17
+ };
18
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
19
+ value: mod,
20
+ enumerable: true
21
+ }) : target, mod));
22
+ //#endregion
23
+ let node_worker_threads = require("node:worker_threads");
24
+ let _deepseek_ai_dsh_llm = require("@deepseek-ai/dsh-llm");
25
+ let node_vm = require("node:vm");
26
+ node_vm = __toESM(node_vm, 1);
27
+ let _deepseek_ai_dsh_session = require("@deepseek-ai/dsh-session");
28
+ let _deepseek_ai_dsh_tools = require("@deepseek-ai/dsh-tools");
29
+ let _deepseek_ai_dsh_workflow = require("@deepseek-ai/dsh-workflow");
30
+ //#region lib/types/protocol.js
31
+ /**
32
+ * The host⇄worker wire protocol: one string-valued enum of message tags per direction, a
33
+ * payload map giving each tag its parameters (the single source of truth), and the message
34
+ * unions derived from them. Payloads are plain JSON by construction for structured clone. Both
35
+ * directions are closed engine protocols whose receivers use `assertNever`; generic typed senders
36
+ * make tag/payload mismatches compile-time errors rather than silently skipped messages.
37
+ * @module @deepseek-ai/dsh-workflow-worker-thread/protocol
38
+ */
39
+ /** Message tags the worker sends the host (the wire values are the tag strings). */
40
+ var WorkerToHostType;
41
+ (function(WorkerToHostType) {
42
+ /** The startup handshake: the session is listening and awaits {@link HostToWorkerType.Go}. */
43
+ WorkerToHostType["Ready"] = "ready";
44
+ /** Observer narration: a `phase(title)` call. */
45
+ WorkerToHostType["Phase"] = "phase";
46
+ /** Observer narration: a `log(message)` call. */
47
+ WorkerToHostType["Log"] = "log";
48
+ /** Observer lifecycle: one `agent()` call started a child. */
49
+ WorkerToHostType["AgentStart"] = "agent-start";
50
+ /** Observer lifecycle: one `agent()` call settled. */
51
+ WorkerToHostType["AgentEnd"] = "agent-end";
52
+ /** Child RPC: start a child on the host (answered by ChildStarted or ChildStartError). */
53
+ WorkerToHostType["ChildStart"] = "child-start";
54
+ /** Child RPC: dispose a started child (answered by ChildDisposed). */
55
+ WorkerToHostType["ChildDispose"] = "child-dispose";
56
+ /** The run's single terminal result. */
57
+ WorkerToHostType["Result"] = "result";
58
+ })(WorkerToHostType || (WorkerToHostType = {}));
59
+ /** Message tags the host sends the worker (the wire values are the tag strings). */
60
+ var HostToWorkerType;
61
+ (function(HostToWorkerType) {
62
+ /** Releases the startup gate: run the script body. */
63
+ HostToWorkerType["Go"] = "go";
64
+ /** Cancel the run: hooks start throwing and the script dies at its next await. */
65
+ HostToWorkerType["Cancel"] = "cancel";
66
+ /** Child RPC reply: the provider fulfilled with a published run (exactly one start reply per ChildStart). */
67
+ HostToWorkerType["ChildStarted"] = "child-started";
68
+ /** Child RPC reply: the provider's asynchronous start failed. */
69
+ HostToWorkerType["ChildStartError"] = "child-start-error";
70
+ /** Child RPC: a started child's result RESOLVED (its JSON projection). */
71
+ HostToWorkerType["ChildSettled"] = "child-settled";
72
+ /** Child RPC: a started child's result REJECTED (an infrastructure fault, rendered). */
73
+ HostToWorkerType["ChildFailed"] = "child-failed";
74
+ /** Child RPC reply: a requested disposal completed. */
75
+ HostToWorkerType["ChildDisposed"] = "child-disposed";
76
+ })(HostToWorkerType || (HostToWorkerType = {}));
77
+ //#endregion
78
+ //#region lib/types/realm.js
79
+ /**
80
+ * Materializes values leaving the script vm into plain JSON before they cross the worker
81
+ * boundary, and renders thrown script values without rejecting the run. The walk rejects
82
+ * values that JSON cannot preserve but trusts model-written workflow scripts: getters and proxy traps may
83
+ * run, and the vm is not a security boundary. The worker provides host-loop isolation and
84
+ * forced termination, not hostile-value containment. See
85
+ * .agents/notes/implemented/feature/2026-07-05-dynamic-workflows.md for the isolation rationale.
86
+ * @module @deepseek-ai/dsh-workflow-worker-thread/realm
87
+ */
88
+ /** Thrown by {@link materializeFromRealm}; the caller wraps it into the right `WorkflowError` code. */
89
+ var MaterializeError = class extends Error {
90
+ path;
91
+ reason;
92
+ constructor(path, reason) {
93
+ super(`${path}: ${reason}`);
94
+ this.path = path;
95
+ this.reason = reason;
96
+ this.name = "MaterializeError";
97
+ }
98
+ };
99
+ /**
100
+ * Render a thrown value to failure text without ever throwing: prefer the
101
+ * `stack` (host or realm — a realm error's `stack` is a plain string read),
102
+ * fall back to `message`, then `String()`. Reading those properties MAY run
103
+ * script code (a getter, `toString`) — accepted under the module's trust
104
+ * premise; if that code itself throws, a fixed label is returned instead.
105
+ * @param error - any value thrown in the host or worker realm.
106
+ * @returns human-readable text for the failure report; prefers the stack.
107
+ */
108
+ function renderThrown(error) {
109
+ try {
110
+ const stack = error?.stack;
111
+ if (typeof stack === "string" && stack.length > 0) return stack;
112
+ const message = error?.message;
113
+ if (typeof message === "string" && message.length > 0) return message;
114
+ return String(error);
115
+ } catch {
116
+ return "[unrenderable thrown value]";
117
+ }
118
+ }
119
+ /**
120
+ * Whether an object's prototype chain represents a plain data object: `null`, or a prototype
121
+ * whose own prototype is `null` (the realm's `Object.prototype` — which we
122
+ * cannot compare by identity across realms). A `Date`/`Map`/class instance
123
+ * has a longer chain and is rejected.
124
+ */
125
+ function hasPlainPrototype(value) {
126
+ const proto = Object.getPrototypeOf(value);
127
+ if (proto === null) return true;
128
+ return Object.getPrototypeOf(proto) === null;
129
+ }
130
+ /**
131
+ * Copy `value` (typically from the vm realm) into plain host JSON data. Root `undefined` is
132
+ * returned unchanged; nested `undefined` and values JSON cannot represent losslessly fail
133
+ * with the offending path. Property accessors run normally, and a throwing read is wrapped
134
+ * with its rendered failure.
135
+ *
136
+ * @param value - the realm value to materialize.
137
+ * @param root - the path label for the root value (error messages).
138
+ * @returns the host-realm copy (plain objects/arrays/scalars only).
139
+ * @throws {@link MaterializeError} for unsupported values, cycles, sparse arrays, exotic
140
+ * prototypes, or property reads that throw.
141
+ */
142
+ function materializeFromRealm(value, root = "value") {
143
+ if (value === void 0) return void 0;
144
+ try {
145
+ return materialize(value, root, /* @__PURE__ */ new Set());
146
+ } catch (error) {
147
+ if (error instanceof MaterializeError) throw error;
148
+ throw new MaterializeError(root, `reading the value threw: ${renderThrown(error)}`);
149
+ }
150
+ }
151
+ function materialize(value, path, seen) {
152
+ switch (typeof value) {
153
+ case "boolean":
154
+ case "string": return value;
155
+ case "number":
156
+ if (!Number.isFinite(value)) throw new MaterializeError(path, "non-finite numbers are not JSON data");
157
+ return value;
158
+ case "bigint": throw new MaterializeError(path, "bigints are not JSON data");
159
+ case "function": throw new MaterializeError(path, "functions are not plain JSON data");
160
+ case "symbol": throw new MaterializeError(path, "symbols are not plain JSON data");
161
+ case "undefined": throw new MaterializeError(path, "undefined is not JSON data");
162
+ case "object": break;
163
+ }
164
+ if (value === null) return null;
165
+ const objectValue = value;
166
+ if (seen.has(objectValue)) throw new MaterializeError(path, "circular references are not JSON data");
167
+ seen.add(objectValue);
168
+ try {
169
+ if (Array.isArray(objectValue)) return materializeArray(objectValue, path, seen);
170
+ return materializeObject(objectValue, path, seen);
171
+ } finally {
172
+ seen.delete(objectValue);
173
+ }
174
+ }
175
+ function materializeArray(value, path, seen) {
176
+ const out = [];
177
+ for (let index = 0; index < value.length; index++) {
178
+ if (!(index in value)) throw new MaterializeError(`${path}[${index}]`, "sparse arrays are not JSON data");
179
+ out.push(materialize(value[index], `${path}[${index}]`, seen));
180
+ }
181
+ for (const key of Object.keys(value)) {
182
+ const index = Number(key);
183
+ if (!Number.isInteger(index) || index < 0 || index >= value.length) throw new MaterializeError(`${path}.${key}`, "arrays with non-index properties are not JSON data");
184
+ }
185
+ if (Object.getOwnPropertySymbols(value).length > 0) throw new MaterializeError(path, "symbol-keyed properties are not plain JSON data");
186
+ return out;
187
+ }
188
+ function materializeObject(value, path, seen) {
189
+ if (!hasPlainPrototype(value)) throw new MaterializeError(path, "only plain objects and arrays are JSON data (exotic prototype)");
190
+ if (Object.getOwnPropertySymbols(value).length > 0) throw new MaterializeError(path, "symbol-keyed properties are not plain JSON data");
191
+ const out = {};
192
+ for (const key of Object.keys(value)) Object.defineProperty(out, key, {
193
+ value: materialize(value[key], `${path}.${key}`, seen),
194
+ enumerable: true,
195
+ writable: true,
196
+ configurable: true
197
+ });
198
+ return out;
199
+ }
200
+ //#endregion
201
+ //#region lib/types/runtime.js
202
+ /**
203
+ * Per-run worker-side vm hooks, child RPC, concurrency/caps, cancellation, and result serialization; it
204
+ * never touches Cordis. Script values leaving the realm are materialized as plain JSON before
205
+ * messaging. Values entering the trusted model-written realm are passed directly; `args` alone is
206
+ * cloned so script mutation cannot alter initialization data. See `./realm.ts` for the trust model.
207
+ *
208
+ * Fatal workflow errors—bad hook arguments, unsupported schemas/options, caps, start failures, and
209
+ * cancellation—propagate through combinators. Only child failures and ordinary stage errors become
210
+ * per-item nulls. Every returned promise has a rejection consumer so dropped script promises cannot
211
+ * kill the worker. A cancelled script that never settles emits nothing; the host force-settles the
212
+ * run within grace and terminates the thread.
213
+ * @module @deepseek-ai/dsh-workflow-worker-thread/runtime
214
+ */
215
+ /** The `agent()` options the script may pass; everything else rejects loud. */
216
+ const SUPPORTED_AGENT_OPTIONS = new Set([
217
+ "label",
218
+ "phase",
219
+ "schema",
220
+ "provider",
221
+ "model"
222
+ ]);
223
+ /** Deferred Claude Code options we name explicitly in the rejection message. */
224
+ const DEFERRED_AGENT_OPTIONS = new Set([
225
+ "effort",
226
+ "isolation",
227
+ "agentType"
228
+ ]);
229
+ /** Flatten a child's final output blocks to text (the non-schema `agent()` result). */
230
+ function outputText(blocks) {
231
+ return blocks.filter((block) => block.type === "text").map((block) => block.text).join("");
232
+ }
233
+ /** A short display label derived from the prompt when the script passes none. */
234
+ function defaultLabel(prompt) {
235
+ const newline = prompt.indexOf("\n");
236
+ const line = newline === -1 ? prompt : prompt.slice(0, newline);
237
+ return line.length <= 48 ? line : `${line.slice(0, 47)}…`;
238
+ }
239
+ /**
240
+ * One live script execution inside the worker. Constructed per run by the
241
+ * session; `drive()` is called exactly once and NEVER rejects — every failure
242
+ * becomes a {@link WorkflowResult} with a non-`completed` stop reason. The
243
+ * host owns cancellation and cleanup of any dropped child work.
244
+ */
245
+ var WorkflowExecution = class {
246
+ limits;
247
+ observer;
248
+ children;
249
+ /** 1-based count of `agent()` calls started (the `agentsStarted` result field). */
250
+ started = 0;
251
+ activeSlots = 0;
252
+ slotWaiters = [];
253
+ cancelReason;
254
+ cancelError;
255
+ currentPhase;
256
+ context;
257
+ compiled;
258
+ constructor(meta, body, args, limits, observer, children) {
259
+ this.limits = limits;
260
+ this.observer = observer;
261
+ this.children = children;
262
+ try {
263
+ this.compiled = new node_vm.Script(`(async () => {\n${body}\n})()`, {
264
+ filename: `workflow:${meta.name}`,
265
+ lineOffset: -1
266
+ });
267
+ } catch (error) {
268
+ throw new _deepseek_ai_dsh_workflow.WorkflowError(`workflow script does not parse: ${String(error)}`, "SCRIPT_PARSE", { cause: error });
269
+ }
270
+ this.context = node_vm.createContext({}, { name: `workflow:${meta.name}` });
271
+ const globals = {
272
+ agent: (prompt, opts) => this.contain(this.agent(prompt, opts)),
273
+ parallel: (thunks) => this.contain(this.parallel(thunks)),
274
+ pipeline: (items, ...stages) => this.contain(this.pipeline(items, stages)),
275
+ phase: (title) => {
276
+ this.phase(title);
277
+ },
278
+ log: (message) => {
279
+ this.log(message);
280
+ },
281
+ args
282
+ };
283
+ for (const [key, value] of Object.entries(globals)) this.context[key] = typeof value === "function" ? Object.freeze(value) : value;
284
+ }
285
+ /**
286
+ * Whether the run has been cancelled. A METHOD, not an inline property
287
+ * read: `cancel()` mutates `cancelReason` concurrently (the session's
288
+ * message handler), and an inline read after an `await` gets narrowed by
289
+ * control flow into an always-false comparison.
290
+ */
291
+ isCancelled() {
292
+ return this.cancelReason !== void 0;
293
+ }
294
+ /**
295
+ * Shared hook entry guard: after {@link cancel}, EVERY hook throws
296
+ * `CANCELLED` at its next call — cancellation is the next HOOK boundary,
297
+ * not just the next `agent()`, so a script that caught one cancelled
298
+ * rejection cannot keep emitting progress through `phase`/`log` or enter a
299
+ * combinator.
300
+ */
301
+ throwIfCancelled() {
302
+ if (this.isCancelled()) throw this.cancelledError();
303
+ }
304
+ /**
305
+ * Cancel the run: waiting `agent()` slots reject and every future hook call
306
+ * throws `CANCELLED` — the script dies at its next await. A script that
307
+ * never settles anyway (parked on a promise no hook owns) is the HOST's
308
+ * problem: its grace timer force-settles the run and terminates the
309
+ * worker. Idempotent; the first reason wins.
310
+ * @param reason - human-readable cause carried on the CANCELLED error. The
311
+ * host independently aborts the required signal shared by every child.
312
+ */
313
+ cancel(reason) {
314
+ if (this.cancelReason !== void 0) return;
315
+ this.cancelReason = reason;
316
+ this.cancelError = new _deepseek_ai_dsh_workflow.WorkflowError(`workflow run cancelled: ${this.cancelReason}`, "CANCELLED");
317
+ for (const waiter of this.slotWaiters.splice(0)) waiter.reject(this.cancelledError());
318
+ }
319
+ /**
320
+ * Run the script to settlement. Resolves — never rejects — with the run's
321
+ * {@link WorkflowResult}: the materialized return value on `completed`, the
322
+ * failure message on `error`, and `cancelled` when the script died of
323
+ * cancellation. This method only chooses the result; the session publishes
324
+ * it and the host owns terminal child cancellation.
325
+ * @returns the settled outcome — this promise NEVER rejects (the seam's
326
+ * `result`-never-rejects contract); every failure maps to a variant.
327
+ */
328
+ async drive() {
329
+ try {
330
+ if (this.isCancelled()) throw this.cancelledError();
331
+ const scriptPromise = this.compiled.runInContext(this.context, { timeout: this.limits.syncTimeoutMs });
332
+ const raw = await this.contain(Promise.resolve(scriptPromise));
333
+ if (this.isCancelled()) throw this.cancelledError();
334
+ return {
335
+ value: raw === void 0 ? null : this.materializeResult(raw),
336
+ stopReason: "completed",
337
+ agentsStarted: this.started
338
+ };
339
+ } catch (error) {
340
+ if (this.isCancelled()) return {
341
+ value: null,
342
+ stopReason: "cancelled",
343
+ error: this.cancelledError().message,
344
+ agentsStarted: this.started
345
+ };
346
+ return {
347
+ value: null,
348
+ stopReason: "error",
349
+ error: renderThrown(error),
350
+ agentsStarted: this.started
351
+ };
352
+ }
353
+ }
354
+ /**
355
+ * Attach a no-op rejection consumer WITHOUT changing what the caller
356
+ * receives: if the script drops the promise (no await), cancellation cannot
357
+ * become an unhandled rejection (which would kill the worker thread); if
358
+ * the script does await it, it still observes the rejection.
359
+ */
360
+ contain(promise) {
361
+ promise.catch(() => {});
362
+ return promise;
363
+ }
364
+ cancelledError() {
365
+ /* v8 ignore next */
366
+ return this.cancelError ?? new _deepseek_ai_dsh_workflow.WorkflowError("workflow run cancelled", "CANCELLED");
367
+ }
368
+ /** Materialize the script's return value; violations become RESULT_UNSERIALIZABLE. */
369
+ materializeResult(raw) {
370
+ try {
371
+ return materializeFromRealm(raw, "workflow result");
372
+ } catch (error) {
373
+ /* v8 ignore next -- defensive rethrow arm: materializeFromRealm only throws MaterializeError */
374
+ if (!(error instanceof MaterializeError)) throw error;
375
+ throw new _deepseek_ai_dsh_workflow.WorkflowError(`the workflow's return value is not plain JSON data — ${error.message}. Return only JSON-serializable objects/arrays/scalars.`, "RESULT_UNSERIALIZABLE", { cause: error });
376
+ }
377
+ }
378
+ /**
379
+ * Acquire one concurrency slot (FIFO). Cancellation rejects QUEUED waiters
380
+ * (see {@link cancel}); the callers guard their own entry and post-acquire
381
+ * windows, so no cancelled-precheck is duplicated here.
382
+ */
383
+ acquireSlot() {
384
+ if (this.activeSlots < this.limits.maxConcurrentAgents) {
385
+ this.activeSlots += 1;
386
+ return Promise.resolve();
387
+ }
388
+ return new Promise((resolve, reject) => {
389
+ this.slotWaiters.push({
390
+ resolve: () => {
391
+ this.activeSlots += 1;
392
+ resolve();
393
+ },
394
+ reject
395
+ });
396
+ });
397
+ }
398
+ releaseSlot() {
399
+ this.activeSlots -= 1;
400
+ const next = this.slotWaiters.shift();
401
+ if (next) next.resolve();
402
+ }
403
+ /** The `agent(prompt, opts)` hook. */
404
+ async agent(rawPrompt, rawOpts) {
405
+ this.throwIfCancelled();
406
+ if (typeof rawPrompt !== "string" || rawPrompt.length === 0) throw new _deepseek_ai_dsh_workflow.WorkflowError("agent() requires a non-empty prompt string", "INVALID_ARGUMENT");
407
+ const opts = this.readAgentOptions(rawOpts);
408
+ if (this.started >= this.limits.maxTotalAgents) throw new _deepseek_ai_dsh_workflow.WorkflowError(`this run reached its total agent cap (${this.limits.maxTotalAgents}) — a runaway-loop backstop; raise the applicable maxTotalAgents limit if the scale is intentional`, "AGENT_CAP");
409
+ this.started += 1;
410
+ const seq = this.started;
411
+ const label = opts.label ?? defaultLabel(rawPrompt);
412
+ const phase = opts.phase ?? this.currentPhase;
413
+ await this.acquireSlot();
414
+ try {
415
+ this.throwIfCancelled();
416
+ let run;
417
+ try {
418
+ run = await this.children.startAgent({
419
+ prompt: rawPrompt,
420
+ ...opts.schema !== void 0 ? { schema: opts.schema } : {},
421
+ ...opts.provider !== void 0 ? { provider: opts.provider } : {},
422
+ ...opts.model !== void 0 ? { model: opts.model } : {}
423
+ });
424
+ } catch (error) {
425
+ if (this.isCancelled()) throw this.cancelledError();
426
+ throw new _deepseek_ai_dsh_workflow.WorkflowError(`agent() could not start a child: ${renderThrown(error)}`, "AGENT_START", { cause: error });
427
+ }
428
+ if (this.isCancelled()) {
429
+ await run.dispose();
430
+ throw this.cancelledError();
431
+ }
432
+ const info = {
433
+ seq,
434
+ label,
435
+ ...phase !== void 0 ? { phase } : {},
436
+ childId: (0, _deepseek_ai_dsh_session.SessionId)(run.id)
437
+ };
438
+ this.observer.agentStart(info);
439
+ try {
440
+ let result;
441
+ try {
442
+ result = await run.result;
443
+ } catch (error) {
444
+ if (this.isCancelled()) {
445
+ this.observer.agentEnd({
446
+ ...info,
447
+ outcome: "cancelled"
448
+ });
449
+ throw this.cancelledError();
450
+ }
451
+ this.observer.agentEnd({
452
+ ...info,
453
+ outcome: "failed"
454
+ });
455
+ throw new _deepseek_ai_dsh_workflow.WorkflowError(`child agent run failed: ${renderThrown(error)}`, "AGENT_RESULT", { cause: error });
456
+ }
457
+ if (result.stopReason === "completed") {
458
+ if (opts.schema !== void 0) {
459
+ if (result.structured === void 0) {
460
+ this.observer.agentEnd({
461
+ ...info,
462
+ outcome: "failed"
463
+ });
464
+ return null;
465
+ }
466
+ this.observer.agentEnd({
467
+ ...info,
468
+ outcome: "completed"
469
+ });
470
+ return result.structured;
471
+ }
472
+ this.observer.agentEnd({
473
+ ...info,
474
+ outcome: "completed"
475
+ });
476
+ return outputText(result.output);
477
+ }
478
+ if (this.isCancelled()) {
479
+ this.observer.agentEnd({
480
+ ...info,
481
+ outcome: "cancelled"
482
+ });
483
+ throw this.cancelledError();
484
+ }
485
+ this.observer.agentEnd({
486
+ ...info,
487
+ outcome: "failed"
488
+ });
489
+ return null;
490
+ } finally {
491
+ await run.dispose();
492
+ }
493
+ } finally {
494
+ this.releaseSlot();
495
+ }
496
+ }
497
+ /** Materialize + validate the `agent()` options bag from the realm. */
498
+ readAgentOptions(rawOpts) {
499
+ if (rawOpts === void 0) return {};
500
+ let opts;
501
+ try {
502
+ opts = materializeFromRealm(rawOpts, "agent() options");
503
+ } catch (error) {
504
+ /* v8 ignore next -- defensive rethrow arm: materializeFromRealm only throws MaterializeError */
505
+ if (!(error instanceof MaterializeError)) throw error;
506
+ throw new _deepseek_ai_dsh_workflow.WorkflowError(`agent() options must be plain JSON data — ${error.message}`, "INVALID_ARGUMENT", { cause: error });
507
+ }
508
+ if (typeof opts !== "object" || opts === null || Array.isArray(opts)) throw new _deepseek_ai_dsh_workflow.WorkflowError("agent() options must be an object", "INVALID_ARGUMENT");
509
+ const record = opts;
510
+ for (const key of Object.keys(record)) {
511
+ if (SUPPORTED_AGENT_OPTIONS.has(key)) continue;
512
+ if (DEFERRED_AGENT_OPTIONS.has(key)) throw new _deepseek_ai_dsh_workflow.WorkflowError(`agent() option "${key}" is deferred and not supported by this engine (supported: label, phase, schema, provider, model)`, "UNSUPPORTED_OPTION");
513
+ throw new _deepseek_ai_dsh_workflow.WorkflowError(`agent() option "${key}" is not recognized (supported: label, phase, schema, provider, model)`, "UNSUPPORTED_OPTION");
514
+ }
515
+ for (const key of [
516
+ "label",
517
+ "phase",
518
+ "provider",
519
+ "model"
520
+ ]) if (record[key] !== void 0 && typeof record[key] !== "string") throw new _deepseek_ai_dsh_workflow.WorkflowError(`agent() option "${key}" must be a string`, "INVALID_ARGUMENT");
521
+ let schema;
522
+ if (record.schema !== void 0) try {
523
+ (0, _deepseek_ai_dsh_tools.assertObjectJsonSchema)(record.schema);
524
+ schema = record.schema;
525
+ } catch (error) {
526
+ /* v8 ignore next -- defensive rethrow arm: assertObjectJsonSchema only throws JsonSchemaError */
527
+ if (!(error instanceof _deepseek_ai_dsh_tools.JsonSchemaError)) throw error;
528
+ throw new _deepseek_ai_dsh_workflow.WorkflowError(`agent() schema is outside the supported subset — ${error.message}`, "UNSUPPORTED_SCHEMA", { cause: error });
529
+ }
530
+ return {
531
+ ...record.label !== void 0 ? { label: record.label } : {},
532
+ ...record.phase !== void 0 ? { phase: record.phase } : {},
533
+ ...record.provider !== void 0 ? { provider: record.provider } : {},
534
+ ...record.model !== void 0 ? { model: record.model } : {},
535
+ ...schema !== void 0 ? { schema } : {}
536
+ };
537
+ }
538
+ /** The `parallel(thunks)` hook: each thunk caught → `null`; fatal errors propagate. */
539
+ async parallel(rawThunks) {
540
+ this.throwIfCancelled();
541
+ if (!Array.isArray(rawThunks)) throw new _deepseek_ai_dsh_workflow.WorkflowError("parallel() requires an array of zero-argument functions", "INVALID_ARGUMENT");
542
+ this.assertItemCap(rawThunks.length, "parallel()");
543
+ const thunks = rawThunks.map((thunk, index) => {
544
+ if (typeof thunk !== "function") throw new _deepseek_ai_dsh_workflow.WorkflowError(`parallel() item ${index} is not a function`, "INVALID_ARGUMENT");
545
+ return thunk;
546
+ });
547
+ return Promise.all(thunks.map(async (thunk) => {
548
+ try {
549
+ return await thunk();
550
+ } catch (error) {
551
+ if ((0, _deepseek_ai_dsh_workflow.isFatalWorkflowError)(error)) throw error;
552
+ return null;
553
+ }
554
+ }));
555
+ }
556
+ /** The `pipeline(items, ...stages)` hook: per-item stage chains, NO cross-stage barrier. */
557
+ async pipeline(rawItems, rawStages) {
558
+ this.throwIfCancelled();
559
+ if (!Array.isArray(rawItems)) throw new _deepseek_ai_dsh_workflow.WorkflowError("pipeline() requires an items array", "INVALID_ARGUMENT");
560
+ this.assertItemCap(rawItems.length, "pipeline()");
561
+ if (rawStages.length === 0) throw new _deepseek_ai_dsh_workflow.WorkflowError("pipeline() requires at least one stage function", "INVALID_ARGUMENT");
562
+ const stages = rawStages.map((stage, index) => {
563
+ if (typeof stage !== "function") throw new _deepseek_ai_dsh_workflow.WorkflowError(`pipeline() stage ${index} is not a function`, "INVALID_ARGUMENT");
564
+ return stage;
565
+ });
566
+ return Promise.all(rawItems.map(async (item, index) => {
567
+ let value = item;
568
+ try {
569
+ for (const stage of stages) value = await stage(value, item, index);
570
+ return value;
571
+ } catch (error) {
572
+ if ((0, _deepseek_ai_dsh_workflow.isFatalWorkflowError)(error)) throw error;
573
+ return null;
574
+ }
575
+ }));
576
+ }
577
+ assertItemCap(length, hook) {
578
+ if (length > this.limits.maxItemsPerCall) throw new _deepseek_ai_dsh_workflow.WorkflowError(`${hook} received ${length} items — over the per-call cap (${this.limits.maxItemsPerCall}); split the work or raise maxItemsPerCall in the engine config`, "ITEM_CAP");
579
+ }
580
+ /** The `phase(title)` hook: sets the current label for subsequent `agent()` calls and notifies observers. */
581
+ phase(title) {
582
+ this.throwIfCancelled();
583
+ if (typeof title !== "string" || title.length === 0) throw new _deepseek_ai_dsh_workflow.WorkflowError("phase() requires a non-empty title string", "INVALID_ARGUMENT");
584
+ this.currentPhase = title;
585
+ this.observer.phase(title);
586
+ }
587
+ /** The `log(message)` hook: narration to observers. */
588
+ log(message) {
589
+ this.throwIfCancelled();
590
+ if (typeof message !== "string") throw new _deepseek_ai_dsh_workflow.WorkflowError("log() requires a message string", "INVALID_ARGUMENT");
591
+ this.observer.log(message);
592
+ }
593
+ };
594
+ //#endregion
595
+ //#region lib/types/session.js
596
+ /**
597
+ * The worker-side half of the engine: {@link runWorkerSession} wires one MessagePort to one
598
+ * {@link WorkflowExecution} — hook progress and child starts go out as messages, run control
599
+ * and child lifecycle come back in — and posts the run's terminal result exactly once. Keeping it
600
+ * separate from `worker.ts` lets unit tests drive the session over a MessageChannel, because main
601
+ * process coverage cannot observe code inside a real Worker.
602
+ *
603
+ * The session announces ready and waits for `go`, so cancellation racing startup can prevent even
604
+ * the script's synchronous prefix. A cancel in place of `go` releases the gate into a cancelled
605
+ * drive without executing the body.
606
+ * @module @deepseek-ai/dsh-workflow-worker-thread/session
607
+ */
608
+ /**
609
+ * The worker-side handle for one started child agent ({@link ChildHandle}):
610
+ * every member is an RPC to the host keyed by this call's `callId`, resolved
611
+ * by the session's message handler through the bridge's pending entry.
612
+ */
613
+ var RpcChildHandle = class {
614
+ post;
615
+ callId;
616
+ entry;
617
+ id;
618
+ result;
619
+ constructor(post, callId, entry, id) {
620
+ this.post = post;
621
+ this.callId = callId;
622
+ this.entry = entry;
623
+ this.id = id;
624
+ this.result = entry.settled.promise;
625
+ }
626
+ dispose() {
627
+ this.post(WorkerToHostType.ChildDispose, { callId: this.callId });
628
+ return this.entry.disposed.promise;
629
+ }
630
+ };
631
+ /**
632
+ * The worker-side child-RPC bridge ({@link ChildPort}): allocates callIds,
633
+ * posts the start/dispose RPCs, and owns the per-call pending
634
+ * book-keeping the session's message handler settles via the `onChild*`
635
+ * entry points.
636
+ */
637
+ var ChildRpcBridge = class {
638
+ post;
639
+ nextCallId = 0;
640
+ pending = /* @__PURE__ */ new Map();
641
+ constructor(post) {
642
+ this.post = post;
643
+ }
644
+ async startAgent(request) {
645
+ this.nextCallId += 1;
646
+ const callId = this.nextCallId;
647
+ const entry = {
648
+ started: Promise.withResolvers(),
649
+ settled: Promise.withResolvers(),
650
+ disposed: Promise.withResolvers()
651
+ };
652
+ entry.settled.promise.catch(() => {});
653
+ this.pending.set(callId, entry);
654
+ this.post(WorkerToHostType.ChildStart, {
655
+ callId,
656
+ request
657
+ });
658
+ const childId = await entry.started.promise;
659
+ return new RpcChildHandle(this.post, callId, entry, childId);
660
+ }
661
+ /** The host established a published child; releases the `startAgent` await. */
662
+ onChildStarted(callId, childId) {
663
+ this.pending.get(callId)?.started.resolve(childId);
664
+ }
665
+ /** Asynchronous provider start failed; reject and retire the pending RPC. */
666
+ onChildStartError(callId, rendered) {
667
+ const entry = this.pending.get(callId);
668
+ this.pending.delete(callId);
669
+ entry?.started.reject(new Error(rendered));
670
+ }
671
+ /** The child's terminal result arrived. */
672
+ onChildSettled(callId, result) {
673
+ this.pending.get(callId)?.settled.resolve(result);
674
+ }
675
+ /** The child's `result` rejected host-side (an infrastructure fault, relayed as fatal). */
676
+ onChildFailed(callId, rendered) {
677
+ this.pending.get(callId)?.settled.reject(new Error(rendered));
678
+ }
679
+ /** The host acked the dispose; the call's book-keeping is complete. */
680
+ onChildDisposed(callId) {
681
+ const entry = this.pending.get(callId);
682
+ this.pending.delete(callId);
683
+ entry?.disposed.resolve();
684
+ }
685
+ };
686
+ /**
687
+ * Narrow the nullable `parentPort` the bootstrap reads from
688
+ * `node:worker_threads`.
689
+ * @param port - `parentPort` as imported (null on the main thread).
690
+ * @returns the port, non-null.
691
+ */
692
+ function requireParentPort(port) {
693
+ if (port === null) throw new Error("the workflow worker entry must be loaded inside a worker thread (no parentPort)");
694
+ return port;
695
+ }
696
+ /**
697
+ * Run one workflow script to settlement against `port`, posting the terminal result message
698
+ * exactly once; resolves after that post (stray children may still be winding down through the
699
+ * port — the host owns their teardown and ultimately terminates the thread). It never rejects:
700
+ * constructor failure becomes an error result. Host pre-parse makes syntax failure here a likely
701
+ * Node-version skew, but the session still reports it instead of dying silently.
702
+ * @param port - the channel to the host (the real `parentPort`, or one side
703
+ * of an in-process `MessageChannel` in tests).
704
+ * @param init - the run payload the host provided as `workerData`.
705
+ */
706
+ async function runWorkerSession(port, init) {
707
+ const post = (type, payload) => {
708
+ port.postMessage({
709
+ type,
710
+ ...payload
711
+ });
712
+ };
713
+ const children = new ChildRpcBridge(post);
714
+ const observer = {
715
+ phase: (title) => {
716
+ post(WorkerToHostType.Phase, { title });
717
+ },
718
+ log: (message) => {
719
+ post(WorkerToHostType.Log, { message });
720
+ },
721
+ agentStart: (info) => {
722
+ post(WorkerToHostType.AgentStart, { info });
723
+ },
724
+ agentEnd: (info) => {
725
+ post(WorkerToHostType.AgentEnd, { info });
726
+ }
727
+ };
728
+ let execution;
729
+ try {
730
+ execution = new WorkflowExecution(init.meta, init.body, init.args, init.limits, observer, children);
731
+ } catch (error) {
732
+ post(WorkerToHostType.Result, { result: {
733
+ value: null,
734
+ stopReason: "error",
735
+ error: renderThrown(error),
736
+ agentsStarted: 0
737
+ } });
738
+ return;
739
+ }
740
+ const gate = Promise.withResolvers();
741
+ port.on("message", (message) => {
742
+ switch (message.type) {
743
+ case HostToWorkerType.Go:
744
+ gate.resolve();
745
+ break;
746
+ case HostToWorkerType.Cancel:
747
+ execution.cancel(message.reason);
748
+ gate.resolve();
749
+ break;
750
+ case HostToWorkerType.ChildStarted:
751
+ children.onChildStarted(message.callId, message.childId);
752
+ break;
753
+ case HostToWorkerType.ChildStartError:
754
+ children.onChildStartError(message.callId, message.rendered);
755
+ break;
756
+ case HostToWorkerType.ChildSettled:
757
+ children.onChildSettled(message.callId, message.result);
758
+ break;
759
+ case HostToWorkerType.ChildFailed:
760
+ children.onChildFailed(message.callId, message.rendered);
761
+ break;
762
+ case HostToWorkerType.ChildDisposed:
763
+ children.onChildDisposed(message.callId);
764
+ break;
765
+ /* v8 ignore next 2 -- closed engine-owned union; the arm only makes adding a message type a compile error */
766
+ default: (0, _deepseek_ai_dsh_llm.assertNever)(message, "host-to-worker message");
767
+ }
768
+ });
769
+ post(WorkerToHostType.Ready, {});
770
+ await gate.promise;
771
+ const result = await execution.drive();
772
+ post(WorkerToHostType.Result, { result });
773
+ }
774
+ //#endregion
775
+ //#region lib/types/worker.js
776
+ /**
777
+ * Single-statement worker entry that boots `runWorkerSession` on real `parentPort`. Logic remains in
778
+ * the session module for in-process MessageChannel coverage; importing this entry on the main thread
779
+ * exercises `requireParentPort`'s failure path.
780
+ * @module @deepseek-ai/dsh-workflow-worker-thread/worker
781
+ */
782
+ runWorkerSession(requireParentPort(node_worker_threads.parentPort), node_worker_threads.workerData);
783
+ //#endregion