@workglow/task-graph 0.0.93 → 0.0.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/node.js CHANGED
@@ -47,19 +47,26 @@ class Dataflow {
47
47
  if (!this.stream)
48
48
  return;
49
49
  const reader = this.stream.getReader();
50
- let accumulatedText = "";
50
+ const accumulatedPorts = new Map;
51
51
  let lastSnapshotData = undefined;
52
52
  let finishData = undefined;
53
53
  let hasTextDelta = false;
54
+ let streamError;
54
55
  try {
55
56
  while (true) {
56
57
  const { done, value: event } = await reader.read();
57
58
  if (done)
58
59
  break;
59
60
  switch (event.type) {
60
- case "text-delta":
61
+ case "text-delta": {
62
+ if (this.sourceTaskPortId !== DATAFLOW_ALL_PORTS && event.port !== this.sourceTaskPortId) {
63
+ break;
64
+ }
61
65
  hasTextDelta = true;
62
- accumulatedText += event.textDelta;
66
+ accumulatedPorts.set(event.port, (accumulatedPorts.get(event.port) ?? "") + event.textDelta);
67
+ break;
68
+ }
69
+ case "object-delta":
63
70
  break;
64
71
  case "snapshot":
65
72
  lastSnapshotData = event.data;
@@ -68,6 +75,7 @@ class Dataflow {
68
75
  finishData = event.data;
69
76
  break;
70
77
  case "error":
78
+ streamError = event.error;
71
79
  break;
72
80
  }
73
81
  }
@@ -75,15 +83,25 @@ class Dataflow {
75
83
  reader.releaseLock();
76
84
  this.stream = undefined;
77
85
  }
86
+ if (streamError) {
87
+ this.error = streamError;
88
+ this.setStatus(TaskStatus.FAILED);
89
+ throw streamError;
90
+ }
78
91
  if (lastSnapshotData !== undefined) {
79
92
  this.setPortData(lastSnapshotData);
80
93
  } else if (finishData && Object.keys(finishData).length > 0) {
81
94
  this.setPortData(finishData);
82
95
  } else if (hasTextDelta) {
83
96
  if (this.sourceTaskPortId === DATAFLOW_ALL_PORTS) {
84
- this.value = { text: accumulatedText };
97
+ const obj = {};
98
+ for (const [port, text] of accumulatedPorts) {
99
+ obj[port] = text;
100
+ }
101
+ this.value = obj;
85
102
  } else {
86
- this.value = accumulatedText;
103
+ const text = accumulatedPorts.values().next().value ?? "";
104
+ this.value = text;
87
105
  }
88
106
  }
89
107
  }
@@ -465,29 +483,54 @@ function getPortStreamMode(schema, portId) {
465
483
  return xStream;
466
484
  return "none";
467
485
  }
468
- function getOutputStreamMode(outputSchema) {
469
- if (typeof outputSchema === "boolean")
470
- return "none";
471
- const props = outputSchema.properties;
486
+ function getStreamingPorts(schema) {
487
+ if (typeof schema === "boolean")
488
+ return [];
489
+ const props = schema.properties;
472
490
  if (!props)
473
- return "none";
474
- let found = "none";
475
- for (const prop of Object.values(props)) {
491
+ return [];
492
+ const result = [];
493
+ for (const [name, prop] of Object.entries(props)) {
476
494
  if (!prop || typeof prop === "boolean")
477
495
  continue;
478
496
  const xStream = prop["x-stream"];
479
- if (xStream === "append")
480
- return "append";
481
- if (xStream === "replace")
482
- found = "replace";
497
+ if (xStream === "append" || xStream === "replace") {
498
+ result.push({ port: name, mode: xStream });
499
+ }
483
500
  }
484
- return found;
501
+ return result;
502
+ }
503
+ function getOutputStreamMode(outputSchema) {
504
+ const ports = getStreamingPorts(outputSchema);
505
+ if (ports.length === 0)
506
+ return "none";
507
+ const mode = ports[0].mode;
508
+ for (let i = 1;i < ports.length; i++) {
509
+ if (ports[i].mode !== mode) {
510
+ throw new Error(`Mixed stream modes on a single task are not supported: ` + `port "${ports[0].port}" is "${mode}" but port "${ports[i].port}" is "${ports[i].mode}"`);
511
+ }
512
+ }
513
+ return mode;
485
514
  }
486
515
  function isTaskStreamable(task) {
487
516
  if (typeof task.executeStream !== "function")
488
517
  return false;
489
518
  return getOutputStreamMode(task.outputSchema()) !== "none";
490
519
  }
520
+ function getAppendPortId(schema) {
521
+ if (typeof schema === "boolean")
522
+ return;
523
+ const props = schema.properties;
524
+ if (!props)
525
+ return;
526
+ for (const [name, prop] of Object.entries(props)) {
527
+ if (!prop || typeof prop === "boolean")
528
+ continue;
529
+ if (prop["x-stream"] === "append")
530
+ return name;
531
+ }
532
+ return;
533
+ }
491
534
  function edgeNeedsAccumulation(sourceSchema, sourcePort, targetSchema, targetPort) {
492
535
  const sourceMode = getPortStreamMode(sourceSchema, sourcePort);
493
536
  if (sourceMode === "none")
@@ -504,6 +547,7 @@ class TaskRunner {
504
547
  abortController;
505
548
  outputCache;
506
549
  registry = globalServiceRegistry;
550
+ inputStreams;
507
551
  constructor(task) {
508
552
  this.task = task;
509
553
  this.own = this.own.bind(this);
@@ -606,7 +650,13 @@ class TaskRunner {
606
650
  }
607
651
  async executeStreamingTask(input) {
608
652
  const streamMode = getOutputStreamMode(this.task.outputSchema());
609
- let accumulated = "";
653
+ if (streamMode === "append") {
654
+ const ports = getStreamingPorts(this.task.outputSchema());
655
+ if (ports.length === 0) {
656
+ throw new TaskError(`Task ${this.task.type} declares append streaming but no output port has x-stream: "append"`);
657
+ }
658
+ }
659
+ const accumulated = new Map;
610
660
  let chunkCount = 0;
611
661
  let finalOutput;
612
662
  this.task.emit("stream_start");
@@ -614,7 +664,8 @@ class TaskRunner {
614
664
  signal: this.abortController.signal,
615
665
  updateProgress: this.handleProgress.bind(this),
616
666
  own: this.own,
617
- registry: this.registry
667
+ registry: this.registry,
668
+ inputStreams: this.inputStreams
618
669
  });
619
670
  for await (const event of stream) {
620
671
  chunkCount++;
@@ -628,20 +679,28 @@ class TaskRunner {
628
679
  this.task.emit("stream_chunk", event);
629
680
  switch (event.type) {
630
681
  case "text-delta": {
631
- accumulated += event.textDelta;
632
- const progress = Math.min(99, Math.round(100 * (1 - Math.exp(-0.02 * chunkCount))));
682
+ accumulated.set(event.port, (accumulated.get(event.port) ?? "") + event.textDelta);
683
+ const progress = Math.min(99, Math.round(100 * (1 - Math.exp(-0.05 * chunkCount))));
684
+ await this.handleProgress(progress);
685
+ break;
686
+ }
687
+ case "object-delta": {
688
+ const progress = Math.min(99, Math.round(100 * (1 - Math.exp(-0.05 * chunkCount))));
633
689
  await this.handleProgress(progress);
634
690
  break;
635
691
  }
636
692
  case "snapshot": {
637
- const progress = Math.min(99, Math.round(100 * (1 - Math.exp(-0.02 * chunkCount))));
693
+ const progress = Math.min(99, Math.round(100 * (1 - Math.exp(-0.05 * chunkCount))));
638
694
  await this.handleProgress(progress);
639
695
  break;
640
696
  }
641
697
  case "finish": {
642
698
  if (streamMode === "append") {
643
- const text = accumulated.length > 0 ? accumulated : event.data?.text ?? "";
644
- finalOutput = { ...event.data || {}, text };
699
+ const merged = { ...event.data || {} };
700
+ for (const [port, text] of accumulated) {
701
+ merged[port] = text.length > 0 ? text : event.data?.[port] ?? "";
702
+ }
703
+ finalOutput = merged;
645
704
  } else if (streamMode === "replace") {
646
705
  finalOutput = event.data;
647
706
  }
@@ -1714,6 +1773,20 @@ class TaskGraphRunner {
1714
1773
  }
1715
1774
  async runTask(task, input) {
1716
1775
  const isStreamable = isTaskStreamable(task);
1776
+ if (isStreamable) {
1777
+ const dataflows = this.graph.getSourceDataflows(task.config.id);
1778
+ const streamingEdges = dataflows.filter((df) => df.stream !== undefined);
1779
+ if (streamingEdges.length > 0) {
1780
+ const inputStreams = new Map;
1781
+ for (const df of streamingEdges) {
1782
+ const stream = df.stream;
1783
+ const [forwardCopy, materializeCopy] = stream.tee();
1784
+ inputStreams.set(df.targetTaskPortId, forwardCopy);
1785
+ df.setStream(materializeCopy);
1786
+ }
1787
+ task.runner.inputStreams = inputStreams;
1788
+ }
1789
+ }
1717
1790
  await this.awaitStreamInputs(task);
1718
1791
  this.copyInputFromEdgesToNode(task);
1719
1792
  if (isStreamable) {
@@ -1781,14 +1854,17 @@ class TaskGraphRunner {
1781
1854
  task.off("stream_end", onStreamEnd);
1782
1855
  }
1783
1856
  }
1784
- pushStreamToEdges(task, streamMode) {
1785
- const targetDataflows = this.graph.getTargetDataflows(task.config.id);
1786
- if (targetDataflows.length === 0)
1787
- return;
1788
- const stream = new ReadableStream({
1857
+ static isPortDelta(event) {
1858
+ return event.type === "text-delta" || event.type === "object-delta";
1859
+ }
1860
+ createStreamFromTaskEvents(task, portId) {
1861
+ return new ReadableStream({
1789
1862
  start: (controller) => {
1790
1863
  const onChunk = (event) => {
1791
1864
  try {
1865
+ if (portId !== undefined && TaskGraphRunner.isPortDelta(event) && event.port !== portId) {
1866
+ return;
1867
+ }
1792
1868
  controller.enqueue(event);
1793
1869
  } catch {}
1794
1870
  };
@@ -1803,17 +1879,36 @@ class TaskGraphRunner {
1803
1879
  task.on("stream_end", onEnd);
1804
1880
  }
1805
1881
  });
1806
- if (targetDataflows.length === 1) {
1807
- targetDataflows[0].setStream(stream);
1808
- } else {
1809
- let currentStream = stream;
1810
- for (let i = 0;i < targetDataflows.length; i++) {
1811
- if (i === targetDataflows.length - 1) {
1812
- targetDataflows[i].setStream(currentStream);
1813
- } else {
1814
- const [s1, s2] = currentStream.tee();
1815
- targetDataflows[i].setStream(s1);
1816
- currentStream = s2;
1882
+ }
1883
+ pushStreamToEdges(task, streamMode) {
1884
+ const targetDataflows = this.graph.getTargetDataflows(task.config.id);
1885
+ if (targetDataflows.length === 0)
1886
+ return;
1887
+ const groups = new Map;
1888
+ for (const df of targetDataflows) {
1889
+ const key = df.sourceTaskPortId;
1890
+ let group = groups.get(key);
1891
+ if (!group) {
1892
+ group = [];
1893
+ groups.set(key, group);
1894
+ }
1895
+ group.push(df);
1896
+ }
1897
+ for (const [portKey, edges] of groups) {
1898
+ const filterPort = portKey === DATAFLOW_ALL_PORTS ? undefined : portKey;
1899
+ const stream = this.createStreamFromTaskEvents(task, filterPort);
1900
+ if (edges.length === 1) {
1901
+ edges[0].setStream(stream);
1902
+ } else {
1903
+ let currentStream = stream;
1904
+ for (let i = 0;i < edges.length; i++) {
1905
+ if (i === edges.length - 1) {
1906
+ edges[i].setStream(currentStream);
1907
+ } else {
1908
+ const [s1, s2] = currentStream.tee();
1909
+ edges[i].setStream(s1);
1910
+ currentStream = s2;
1911
+ }
1817
1912
  }
1818
1913
  }
1819
1914
  }
@@ -2144,6 +2239,69 @@ class GraphAsTask extends Task {
2144
2239
  });
2145
2240
  }
2146
2241
  }
2242
+ async* executeStream(input, context) {
2243
+ if (context.inputStreams) {
2244
+ for (const [, stream] of context.inputStreams) {
2245
+ const reader = stream.getReader();
2246
+ try {
2247
+ while (true) {
2248
+ const { done, value } = await reader.read();
2249
+ if (done)
2250
+ break;
2251
+ if (value.type === "finish")
2252
+ continue;
2253
+ yield value;
2254
+ }
2255
+ } finally {
2256
+ reader.releaseLock();
2257
+ }
2258
+ }
2259
+ }
2260
+ if (this.hasChildren()) {
2261
+ const endingNodeIds = new Set;
2262
+ const tasks = this.subGraph.getTasks();
2263
+ for (const task of tasks) {
2264
+ if (this.subGraph.getTargetDataflows(task.config.id).length === 0) {
2265
+ endingNodeIds.add(task.config.id);
2266
+ }
2267
+ }
2268
+ const eventQueue = [];
2269
+ let resolveWaiting;
2270
+ let subgraphDone = false;
2271
+ const unsub = this.subGraph.subscribeToTaskStreaming({
2272
+ onStreamChunk: (taskId, event) => {
2273
+ if (endingNodeIds.has(taskId) && event.type !== "finish") {
2274
+ eventQueue.push(event);
2275
+ resolveWaiting?.();
2276
+ }
2277
+ }
2278
+ });
2279
+ const runPromise = this.subGraph.run(input, { parentSignal: context.signal }).then((results2) => {
2280
+ subgraphDone = true;
2281
+ resolveWaiting?.();
2282
+ return results2;
2283
+ });
2284
+ while (!subgraphDone) {
2285
+ if (eventQueue.length === 0) {
2286
+ await new Promise((resolve) => {
2287
+ resolveWaiting = resolve;
2288
+ });
2289
+ }
2290
+ while (eventQueue.length > 0) {
2291
+ yield eventQueue.shift();
2292
+ }
2293
+ }
2294
+ while (eventQueue.length > 0) {
2295
+ yield eventQueue.shift();
2296
+ }
2297
+ unsub();
2298
+ const results = await runPromise;
2299
+ const mergedOutput = this.subGraph.mergeExecuteOutputsToRunOutput(results, this.compoundMerge);
2300
+ yield { type: "finish", data: mergedOutput };
2301
+ } else {
2302
+ yield { type: "finish", data: input };
2303
+ }
2304
+ }
2147
2305
  regenerateGraph() {
2148
2306
  this._inputSchemaNode = undefined;
2149
2307
  this.events.emit("regenerate");
@@ -3436,6 +3594,9 @@ class IteratorTask extends GraphAsTask {
3436
3594
  }
3437
3595
  return this._runner;
3438
3596
  }
3597
+ async* executeStream(input, _context) {
3598
+ yield { type: "finish", data: input };
3599
+ }
3439
3600
  set subGraph(subGraph) {
3440
3601
  super.subGraph = subGraph;
3441
3602
  this.invalidateIterationInputSchema();
@@ -3931,6 +4092,50 @@ class WhileTask extends GraphAsTask {
3931
4092
  }
3932
4093
  return currentOutput;
3933
4094
  }
4095
+ async* executeStream(input, context) {
4096
+ if (!this.hasChildren()) {
4097
+ throw new TaskConfigurationError(`${this.type}: No subgraph set for while loop`);
4098
+ }
4099
+ const condition = this.condition ?? this.buildConditionFromExtras();
4100
+ if (!condition) {
4101
+ throw new TaskConfigurationError(`${this.type}: No condition function provided`);
4102
+ }
4103
+ const arrayAnalysis = this.analyzeArrayInputs(input);
4104
+ this._currentIteration = 0;
4105
+ let currentInput = { ...input };
4106
+ let currentOutput = {};
4107
+ const effectiveMax = arrayAnalysis ? Math.min(this.maxIterations, arrayAnalysis.iterationCount) : this.maxIterations;
4108
+ while (this._currentIteration < effectiveMax) {
4109
+ if (context.signal?.aborted)
4110
+ break;
4111
+ let iterationInput;
4112
+ if (arrayAnalysis) {
4113
+ iterationInput = {
4114
+ ...this.buildIterationInput(currentInput, arrayAnalysis, this._currentIteration),
4115
+ _iterationIndex: this._currentIteration
4116
+ };
4117
+ } else {
4118
+ iterationInput = {
4119
+ ...currentInput,
4120
+ _iterationIndex: this._currentIteration
4121
+ };
4122
+ }
4123
+ const results = await this.subGraph.run(iterationInput, {
4124
+ parentSignal: context.signal
4125
+ });
4126
+ currentOutput = this.subGraph.mergeExecuteOutputsToRunOutput(results, this.compoundMerge);
4127
+ if (!condition(currentOutput, this._currentIteration)) {
4128
+ break;
4129
+ }
4130
+ if (this.chainIterations) {
4131
+ currentInput = { ...currentInput, ...currentOutput };
4132
+ }
4133
+ this._currentIteration++;
4134
+ const progress = Math.min(this._currentIteration / effectiveMax * 100, 99);
4135
+ await context.updateProgress(progress, `Iteration ${this._currentIteration}`);
4136
+ }
4137
+ yield { type: "finish", data: currentOutput };
4138
+ }
3934
4139
  getIterationContextSchema() {
3935
4140
  return this.constructor.getIterationContextSchema();
3936
4141
  }
@@ -4904,6 +5109,7 @@ export {
4904
5109
  hasVectorOutput,
4905
5110
  hasVectorLikeInput,
4906
5111
  getTaskQueueRegistry,
5112
+ getStreamingPorts,
4907
5113
  getPortStreamMode,
4908
5114
  getOutputStreamMode,
4909
5115
  getNestedValue,
@@ -4911,6 +5117,7 @@ export {
4911
5117
  getJobQueueFactory,
4912
5118
  getIterationContextSchemaForType,
4913
5119
  getInputModeFromSchema,
5120
+ getAppendPortId,
4914
5121
  findArrayPorts,
4915
5122
  filterIterationProperties,
4916
5123
  extractIterationProperties,
@@ -4980,4 +5187,4 @@ export {
4980
5187
  ConditionalTask
4981
5188
  };
4982
5189
 
4983
- //# debugId=4E7D701E306189E464756E2164756E21
5190
+ //# debugId=D1D0735E2B61108764756E2164756E21