@workglow/task-graph 0.0.92 → 0.0.94

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