vellum-ai 0.10.8__py3-none-any.whl → 0.11.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. vellum/client/core/client_wrapper.py +1 -1
  2. vellum/client/types/logical_operator.py +2 -0
  3. vellum/evaluations/resources.py +7 -12
  4. vellum/evaluations/utils/env.py +1 -3
  5. vellum/evaluations/utils/paginator.py +0 -1
  6. vellum/evaluations/utils/typing.py +1 -1
  7. vellum/evaluations/utils/uuid.py +1 -1
  8. vellum/plugins/vellum_mypy.py +3 -1
  9. vellum/workflows/descriptors/utils.py +27 -0
  10. vellum/workflows/events/__init__.py +0 -2
  11. vellum/workflows/events/node.py +7 -6
  12. vellum/workflows/events/tests/test_event.py +2 -2
  13. vellum/workflows/events/types.py +35 -30
  14. vellum/workflows/events/workflow.py +33 -8
  15. vellum/workflows/nodes/bases/base.py +49 -26
  16. vellum/workflows/nodes/bases/tests/test_base_node.py +0 -1
  17. vellum/workflows/nodes/core/templating_node/node.py +1 -0
  18. vellum/workflows/nodes/core/try_node/node.py +22 -4
  19. vellum/workflows/nodes/core/try_node/tests/test_node.py +16 -3
  20. vellum/workflows/nodes/displayable/bases/api_node/node.py +1 -1
  21. vellum/workflows/nodes/displayable/bases/base_prompt_node/node.py +0 -1
  22. vellum/workflows/nodes/displayable/bases/inline_prompt_node/node.py +0 -1
  23. vellum/workflows/nodes/displayable/bases/prompt_deployment_node.py +2 -1
  24. vellum/workflows/nodes/displayable/bases/search_node.py +0 -1
  25. vellum/workflows/nodes/displayable/code_execution_node/tests/test_code_execution_node.py +0 -1
  26. vellum/workflows/nodes/displayable/code_execution_node/utils.py +3 -2
  27. vellum/workflows/nodes/displayable/conditional_node/node.py +1 -1
  28. vellum/workflows/nodes/displayable/guardrail_node/node.py +0 -1
  29. vellum/workflows/nodes/displayable/inline_prompt_node/node.py +1 -0
  30. vellum/workflows/nodes/displayable/prompt_deployment_node/node.py +3 -1
  31. vellum/workflows/nodes/displayable/search_node/node.py +1 -0
  32. vellum/workflows/nodes/displayable/subworkflow_deployment_node/node.py +3 -2
  33. vellum/workflows/nodes/displayable/tests/test_inline_text_prompt_node.py +10 -7
  34. vellum/workflows/nodes/displayable/tests/test_search_node_wth_text_output.py +0 -1
  35. vellum/workflows/outputs/base.py +2 -4
  36. vellum/workflows/ports/node_ports.py +1 -1
  37. vellum/workflows/runner/runner.py +185 -157
  38. vellum/workflows/state/base.py +55 -23
  39. vellum/workflows/state/context.py +26 -3
  40. vellum/workflows/types/core.py +1 -0
  41. vellum/workflows/types/tests/test_utils.py +1 -0
  42. vellum/workflows/types/utils.py +0 -1
  43. vellum/workflows/utils/functions.py +74 -0
  44. vellum/workflows/utils/tests/test_functions.py +171 -0
  45. vellum/workflows/utils/tests/test_vellum_variables.py +0 -1
  46. vellum/workflows/utils/vellum_variables.py +2 -2
  47. vellum/workflows/workflows/base.py +84 -10
  48. vellum/workflows/workflows/event_filters.py +53 -0
  49. {vellum_ai-0.10.8.dist-info → vellum_ai-0.11.0.dist-info}/METADATA +1 -1
  50. {vellum_ai-0.10.8.dist-info → vellum_ai-0.11.0.dist-info}/RECORD +101 -93
  51. vellum_cli/__init__.py +147 -13
  52. vellum_cli/config.py +0 -1
  53. vellum_cli/image_push.py +1 -1
  54. vellum_cli/pull.py +29 -19
  55. vellum_cli/push.py +9 -10
  56. vellum_cli/tests/__init__.py +0 -0
  57. vellum_cli/tests/conftest.py +40 -0
  58. vellum_cli/tests/test_main.py +11 -0
  59. vellum_cli/tests/test_pull.py +125 -71
  60. vellum_cli/tests/test_push.py +173 -0
  61. vellum_ee/workflows/display/nodes/base_node_display.py +3 -2
  62. vellum_ee/workflows/display/nodes/base_node_vellum_display.py +2 -2
  63. vellum_ee/workflows/display/nodes/get_node_display_class.py +1 -1
  64. vellum_ee/workflows/display/nodes/tests/test_base_node_display.py +1 -1
  65. vellum_ee/workflows/display/nodes/vellum/__init__.py +5 -3
  66. vellum_ee/workflows/display/nodes/vellum/api_node.py +4 -7
  67. vellum_ee/workflows/display/nodes/vellum/conditional_node.py +39 -22
  68. vellum_ee/workflows/display/nodes/vellum/error_node.py +49 -0
  69. vellum_ee/workflows/display/nodes/vellum/final_output_node.py +0 -2
  70. vellum_ee/workflows/display/nodes/vellum/guardrail_node.py +1 -1
  71. vellum_ee/workflows/display/nodes/vellum/inline_prompt_node.py +1 -1
  72. vellum_ee/workflows/display/nodes/vellum/inline_subworkflow_node.py +4 -2
  73. vellum_ee/workflows/display/nodes/vellum/map_node.py +11 -5
  74. vellum_ee/workflows/display/nodes/vellum/merge_node.py +2 -2
  75. vellum_ee/workflows/display/nodes/vellum/note_node.py +1 -3
  76. vellum_ee/workflows/display/nodes/vellum/prompt_deployment_node.py +1 -1
  77. vellum_ee/workflows/display/nodes/vellum/search_node.py +1 -1
  78. vellum_ee/workflows/display/nodes/vellum/subworkflow_deployment_node.py +1 -1
  79. vellum_ee/workflows/display/nodes/vellum/templating_node.py +1 -1
  80. vellum_ee/workflows/display/nodes/vellum/tests/test_utils.py +5 -5
  81. vellum_ee/workflows/display/nodes/vellum/utils.py +4 -4
  82. vellum_ee/workflows/display/tests/test_vellum_workflow_display.py +45 -0
  83. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_api_node_serialization.py +13 -24
  84. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_conditional_node_serialization.py +13 -39
  85. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_error_node_serialization.py +203 -0
  86. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_guardrail_node_serialization.py +2 -2
  87. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_inline_subworkflow_serialization.py +62 -58
  88. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_map_node_serialization.py +25 -4
  89. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_merge_node_serialization.py +2 -1
  90. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_prompt_deployment_serialization.py +2 -2
  91. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_subworkflow_deployment_serialization.py +2 -2
  92. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_terminal_node_serialization.py +1 -1
  93. vellum_ee/workflows/display/tests/workflow_serialization/test_basic_try_node_serialization.py +2 -1
  94. vellum_ee/workflows/display/tests/workflow_serialization/test_complex_terminal_node_serialization.py +2 -2
  95. vellum_ee/workflows/display/types.py +4 -4
  96. vellum_ee/workflows/display/utils/vellum.py +2 -6
  97. vellum_ee/workflows/display/workflows/get_vellum_workflow_display_class.py +4 -1
  98. vellum_ee/workflows/display/workflows/vellum_workflow_display.py +6 -2
  99. vellum/workflows/events/utils.py +0 -5
  100. vellum/workflows/runner/types.py +0 -16
  101. {vellum_ai-0.10.8.dist-info → vellum_ai-0.11.0.dist-info}/LICENSE +0 -0
  102. {vellum_ai-0.10.8.dist-info → vellum_ai-0.11.0.dist-info}/WHEEL +0 -0
  103. {vellum_ai-0.10.8.dist-info → vellum_ai-0.11.0.dist-info}/entry_points.txt +0 -0
@@ -3,7 +3,7 @@ from copy import deepcopy
3
3
  import logging
4
4
  from queue import Empty, Queue
5
5
  from threading import Event as ThreadingEvent, Thread
6
- from uuid import UUID, uuid4
6
+ from uuid import UUID
7
7
  from typing import TYPE_CHECKING, Any, Dict, Generic, Iterable, Iterator, Optional, Sequence, Set, Type, Union
8
8
 
9
9
  from vellum.workflows.constants import UNDEF
@@ -29,7 +29,6 @@ from vellum.workflows.events.node import (
29
29
  NodeExecutionStreamingBody,
30
30
  )
31
31
  from vellum.workflows.events.types import BaseEvent, ParentContext, WorkflowParentContext
32
- from vellum.workflows.events.utils import is_terminal_event
33
32
  from vellum.workflows.events.workflow import (
34
33
  WorkflowExecutionFulfilledBody,
35
34
  WorkflowExecutionInitiatedBody,
@@ -38,6 +37,8 @@ from vellum.workflows.events.workflow import (
38
37
  WorkflowExecutionRejectedBody,
39
38
  WorkflowExecutionResumedBody,
40
39
  WorkflowExecutionResumedEvent,
40
+ WorkflowExecutionSnapshottedBody,
41
+ WorkflowExecutionSnapshottedEvent,
41
42
  WorkflowExecutionStreamingBody,
42
43
  )
43
44
  from vellum.workflows.exceptions import NodeException
@@ -46,7 +47,6 @@ from vellum.workflows.outputs import BaseOutputs
46
47
  from vellum.workflows.outputs.base import BaseOutput
47
48
  from vellum.workflows.ports.port import Port
48
49
  from vellum.workflows.references import ExternalInputReference, OutputReference
49
- from vellum.workflows.runner.types import WorkItemEvent
50
50
  from vellum.workflows.state.base import BaseState
51
51
  from vellum.workflows.types.generics import OutputsType, StateType, WorkflowInputsType
52
52
 
@@ -77,6 +77,7 @@ class WorkflowRunner(Generic[StateType]):
77
77
  raise ValueError("Can only run a Workflow providing one of state or external inputs, not both")
78
78
 
79
79
  self.workflow = workflow
80
+ self._is_resuming = False
80
81
  if entrypoint_nodes:
81
82
  if len(list(entrypoint_nodes)) > 1:
82
83
  raise ValueError("Cannot resume from multiple nodes")
@@ -99,6 +100,7 @@ class WorkflowRunner(Generic[StateType]):
99
100
  for ei in external_inputs
100
101
  if issubclass(ei.inputs_class.__parent_class__, BaseNode)
101
102
  ]
103
+ self._is_resuming = True
102
104
  else:
103
105
  normalized_inputs = deepcopy(inputs) if inputs else self.workflow.get_default_inputs()
104
106
  if state:
@@ -108,9 +110,16 @@ class WorkflowRunner(Generic[StateType]):
108
110
  self._initial_state = self.workflow.get_default_state(normalized_inputs)
109
111
  self._entrypoints = self.workflow.get_entrypoints()
110
112
 
111
- self._work_item_event_queue: Queue[WorkItemEvent[StateType]] = Queue()
112
- self._workflow_event_queue: Queue[WorkflowEvent] = Queue()
113
+ # This queue is responsible for sending events from WorkflowRunner to the outside world
114
+ self._workflow_event_outer_queue: Queue[WorkflowEvent] = Queue()
115
+
116
+ # This queue is responsible for sending events from the inner worker threads to WorkflowRunner
117
+ self._workflow_event_inner_queue: Queue[WorkflowEvent] = Queue()
118
+
119
+ # This queue is responsible for sending events from WorkflowRunner to the background thread
120
+ # for user defined emitters
113
121
  self._background_thread_queue: Queue[BackgroundThreadItem] = Queue()
122
+
114
123
  self._dependencies: Dict[Type[BaseNode], Set[Type[BaseNode]]] = defaultdict(set)
115
124
  self._state_forks: Set[StateType] = {self._initial_state}
116
125
 
@@ -123,8 +132,20 @@ class WorkflowRunner(Generic[StateType]):
123
132
  "__snapshot_callback__",
124
133
  lambda s: self._snapshot_state(s),
125
134
  )
135
+ self.workflow.context._register_event_queue(self._workflow_event_inner_queue)
126
136
 
127
137
  def _snapshot_state(self, state: StateType) -> StateType:
138
+ self._workflow_event_inner_queue.put(
139
+ WorkflowExecutionSnapshottedEvent(
140
+ trace_id=state.meta.trace_id,
141
+ span_id=state.meta.span_id,
142
+ body=WorkflowExecutionSnapshottedBody(
143
+ workflow_definition=self.workflow.__class__,
144
+ state=state,
145
+ ),
146
+ parent=self._parent_context,
147
+ )
148
+ )
128
149
  self.workflow._store.append_state_snapshot(state)
129
150
  self._background_thread_queue.put(state)
130
151
  return state
@@ -135,21 +156,19 @@ class WorkflowRunner(Generic[StateType]):
135
156
  return event
136
157
 
137
158
  def _run_work_item(self, node: BaseNode[StateType], span_id: UUID) -> None:
138
- self._work_item_event_queue.put(
139
- WorkItemEvent(
140
- node=node,
141
- event=NodeExecutionInitiatedEvent(
142
- trace_id=node.state.meta.trace_id,
159
+ self._workflow_event_inner_queue.put(
160
+ NodeExecutionInitiatedEvent(
161
+ trace_id=node.state.meta.trace_id,
162
+ span_id=span_id,
163
+ body=NodeExecutionInitiatedBody(
164
+ node_definition=node.__class__,
165
+ inputs=node._inputs,
166
+ ),
167
+ parent=WorkflowParentContext(
143
168
  span_id=span_id,
144
- body=NodeExecutionInitiatedBody(
145
- node_definition=node.__class__,
146
- inputs=node._inputs,
147
- ),
148
- parent=WorkflowParentContext(
149
- span_id=span_id,
150
- workflow_definition=self.workflow.__class__,
151
- parent=self._parent_context
152
- )
169
+ workflow_definition=self.workflow.__class__,
170
+ parent=self._parent_context,
171
+ type="WORKFLOW",
153
172
  ),
154
173
  )
155
174
  )
@@ -186,24 +205,23 @@ class WorkflowRunner(Generic[StateType]):
186
205
  outputs_class=node.Outputs,
187
206
  )
188
207
  node.state.meta.node_outputs[output_descriptor] = streaming_output_queues[output.name]
189
- self._work_item_event_queue.put(
190
- WorkItemEvent(
191
- node=node,
192
- event=NodeExecutionStreamingEvent(
193
- trace_id=node.state.meta.trace_id,
208
+ initiated_output: BaseOutput = BaseOutput(name=output.name)
209
+ initiated_ports = initiated_output > ports
210
+ self._workflow_event_inner_queue.put(
211
+ NodeExecutionStreamingEvent(
212
+ trace_id=node.state.meta.trace_id,
213
+ span_id=span_id,
214
+ body=NodeExecutionStreamingBody(
215
+ node_definition=node.__class__,
216
+ output=initiated_output,
217
+ invoked_ports=initiated_ports,
218
+ ),
219
+ parent=WorkflowParentContext(
194
220
  span_id=span_id,
195
- body=NodeExecutionStreamingBody(
196
- node_definition=node.__class__,
197
- output=BaseOutput(name=output.name),
198
- invoked_ports=invoked_ports,
199
- ),
200
- parent=WorkflowParentContext(
201
- span_id=span_id,
202
- workflow_definition=self.workflow.__class__,
203
- parent=self._parent_context
204
- )
221
+ workflow_definition=self.workflow.__class__,
222
+ parent=self._parent_context,
205
223
  ),
206
- )
224
+ ),
207
225
  )
208
226
 
209
227
  for output in node_run_response:
@@ -215,50 +233,47 @@ class WorkflowRunner(Generic[StateType]):
215
233
  initiate_node_streaming_output(output)
216
234
 
217
235
  streaming_output_queues[output.name].put(output.delta)
218
- self._work_item_event_queue.put(
219
- WorkItemEvent(
220
- node=node,
221
- event=NodeExecutionStreamingEvent(
222
- trace_id=node.state.meta.trace_id,
236
+ self._workflow_event_inner_queue.put(
237
+ NodeExecutionStreamingEvent(
238
+ trace_id=node.state.meta.trace_id,
239
+ span_id=span_id,
240
+ body=NodeExecutionStreamingBody(
241
+ node_definition=node.__class__,
242
+ output=output,
243
+ invoked_ports=invoked_ports,
244
+ ),
245
+ parent=WorkflowParentContext(
223
246
  span_id=span_id,
224
- body=NodeExecutionStreamingBody(
225
- node_definition=node.__class__,
226
- output=output,
227
- invoked_ports=invoked_ports,
228
- ),
229
- parent=WorkflowParentContext(
230
- span_id=span_id,
231
- workflow_definition=self.workflow.__class__,
232
- parent=self._parent_context,
233
- )
247
+ workflow_definition=self.workflow.__class__,
248
+ parent=self._parent_context,
234
249
  ),
235
- )
250
+ ),
236
251
  )
237
252
  elif output.is_fulfilled:
238
253
  if output.name in streaming_output_queues:
239
254
  streaming_output_queues[output.name].put(UNDEF)
240
255
 
241
256
  setattr(outputs, output.name, output.value)
242
- self._work_item_event_queue.put(
243
- WorkItemEvent(
244
- node=node,
245
- event=NodeExecutionStreamingEvent(
246
- trace_id=node.state.meta.trace_id,
257
+ self._workflow_event_inner_queue.put(
258
+ NodeExecutionStreamingEvent(
259
+ trace_id=node.state.meta.trace_id,
260
+ span_id=span_id,
261
+ body=NodeExecutionStreamingBody(
262
+ node_definition=node.__class__,
263
+ output=output,
264
+ invoked_ports=invoked_ports,
265
+ ),
266
+ parent=WorkflowParentContext(
247
267
  span_id=span_id,
248
- body=NodeExecutionStreamingBody(
249
- node_definition=node.__class__,
250
- output=output,
251
- invoked_ports=invoked_ports,
252
- ),
253
- parent=WorkflowParentContext(
254
- span_id=span_id,
255
- workflow_definition=self.workflow.__class__,
256
- parent=self._parent_context,
257
- )
268
+ workflow_definition=self.workflow.__class__,
269
+ parent=self._parent_context,
258
270
  ),
259
271
  )
260
272
  )
261
273
 
274
+ invoked_ports = ports(outputs, node.state)
275
+ node.state.meta.node_execution_cache.fulfill_node_execution(node.__class__, span_id)
276
+
262
277
  for descriptor, output_value in outputs:
263
278
  if output_value is UNDEF:
264
279
  if descriptor in node.state.meta.node_outputs:
@@ -267,70 +282,58 @@ class WorkflowRunner(Generic[StateType]):
267
282
 
268
283
  node.state.meta.node_outputs[descriptor] = output_value
269
284
 
270
- invoked_ports = ports(outputs, node.state)
271
- node.state.meta.node_execution_cache.fulfill_node_execution(node.__class__, span_id)
272
-
273
- self._work_item_event_queue.put(
274
- WorkItemEvent(
275
- node=node,
276
- event=NodeExecutionFulfilledEvent(
277
- trace_id=node.state.meta.trace_id,
285
+ self._workflow_event_inner_queue.put(
286
+ NodeExecutionFulfilledEvent(
287
+ trace_id=node.state.meta.trace_id,
288
+ span_id=span_id,
289
+ body=NodeExecutionFulfilledBody(
290
+ node_definition=node.__class__,
291
+ outputs=outputs,
292
+ invoked_ports=invoked_ports,
293
+ ),
294
+ parent=WorkflowParentContext(
278
295
  span_id=span_id,
279
- body=NodeExecutionFulfilledBody(
280
- node_definition=node.__class__,
281
- outputs=outputs,
282
- invoked_ports=invoked_ports,
283
- ),
284
- parent=WorkflowParentContext(
285
- span_id=span_id,
286
- workflow_definition=self.workflow.__class__,
287
- parent=self._parent_context,
288
- )
296
+ workflow_definition=self.workflow.__class__,
297
+ parent=self._parent_context,
289
298
  ),
290
299
  )
291
300
  )
292
301
  except NodeException as e:
293
- self._work_item_event_queue.put(
294
- WorkItemEvent(
295
- node=node,
296
- event=NodeExecutionRejectedEvent(
297
- trace_id=node.state.meta.trace_id,
302
+ self._workflow_event_inner_queue.put(
303
+ NodeExecutionRejectedEvent(
304
+ trace_id=node.state.meta.trace_id,
305
+ span_id=span_id,
306
+ body=NodeExecutionRejectedBody(
307
+ node_definition=node.__class__,
308
+ error=e.error,
309
+ ),
310
+ parent=WorkflowParentContext(
298
311
  span_id=span_id,
299
- body=NodeExecutionRejectedBody(
300
- node_definition=node.__class__,
301
- error=e.error,
302
- ),
303
- parent=WorkflowParentContext(
304
- span_id=span_id,
305
- workflow_definition=self.workflow.__class__,
306
- parent=self._parent_context,
307
- )
312
+ workflow_definition=self.workflow.__class__,
313
+ parent=self._parent_context,
308
314
  ),
309
315
  )
310
316
  )
311
317
  except Exception as e:
312
318
  logger.exception(f"An unexpected error occurred while running node {node.__class__.__name__}")
313
319
 
314
- self._work_item_event_queue.put(
315
- WorkItemEvent(
316
- node=node,
317
- event=NodeExecutionRejectedEvent(
318
- trace_id=node.state.meta.trace_id,
319
- span_id=span_id,
320
- body=NodeExecutionRejectedBody(
321
- node_definition=node.__class__,
322
- error=VellumError(
323
- message=str(e),
324
- code=VellumErrorCode.INTERNAL_ERROR,
325
- ),
320
+ self._workflow_event_inner_queue.put(
321
+ NodeExecutionRejectedEvent(
322
+ trace_id=node.state.meta.trace_id,
323
+ span_id=span_id,
324
+ body=NodeExecutionRejectedBody(
325
+ node_definition=node.__class__,
326
+ error=VellumError(
327
+ message=str(e),
328
+ code=VellumErrorCode.INTERNAL_ERROR,
326
329
  ),
327
- parent=WorkflowParentContext(
328
- span_id=span_id,
329
- workflow_definition=self.workflow.__class__,
330
- parent=self._parent_context
331
- )
332
330
  ),
333
- )
331
+ parent=WorkflowParentContext(
332
+ span_id=span_id,
333
+ workflow_definition=self.workflow.__class__,
334
+ parent=self._parent_context,
335
+ ),
336
+ ),
334
337
  )
335
338
 
336
339
  logger.debug(f"Finished running node: {node.__class__.__name__}")
@@ -350,7 +353,10 @@ class WorkflowRunner(Generic[StateType]):
350
353
  self._run_node_if_ready(next_state, edge.to_node, edge)
351
354
 
352
355
  def _run_node_if_ready(
353
- self, state: StateType, node_class: Type[BaseNode], invoked_by: Optional[Edge] = None
356
+ self,
357
+ state: StateType,
358
+ node_class: Type[BaseNode],
359
+ invoked_by: Optional[Edge] = None,
354
360
  ) -> None:
355
361
  with state.__lock__:
356
362
  for descriptor in node_class.ExternalInputs:
@@ -362,22 +368,23 @@ class WorkflowRunner(Generic[StateType]):
362
368
  return
363
369
 
364
370
  all_deps = self._dependencies[node_class]
365
- if not node_class.Trigger.should_initiate(state, all_deps, invoked_by):
371
+ node_span_id = state.meta.node_execution_cache.queue_node_execution(node_class, all_deps, invoked_by)
372
+ if not node_class.Trigger.should_initiate(state, all_deps, node_span_id):
366
373
  return
367
374
 
368
375
  node = node_class(state=state, context=self.workflow.context)
369
- node_span_id = uuid4()
370
376
  state.meta.node_execution_cache.initiate_node_execution(node_class, node_span_id)
371
377
  self._active_nodes_by_execution_id[node_span_id] = node
372
378
 
373
- worker_thread = Thread(target=self._run_work_item, kwargs={"node": node, "span_id": node_span_id})
379
+ worker_thread = Thread(
380
+ target=self._run_work_item,
381
+ kwargs={"node": node, "span_id": node_span_id},
382
+ )
374
383
  worker_thread.start()
375
384
 
376
- def _handle_work_item_event(self, work_item_event: WorkItemEvent[StateType]) -> Optional[VellumError]:
377
- node = work_item_event.node
378
- event = work_item_event.event
379
-
380
- if event.name == "node.execution.initiated":
385
+ def _handle_work_item_event(self, event: WorkflowEvent) -> Optional[VellumError]:
386
+ node = self._active_nodes_by_execution_id.get(event.span_id)
387
+ if not node:
381
388
  return None
382
389
 
383
390
  if event.name == "node.execution.rejected":
@@ -394,7 +401,7 @@ class WorkflowRunner(Generic[StateType]):
394
401
  if node_output_descriptor.name != event.output.name:
395
402
  continue
396
403
 
397
- self._workflow_event_queue.put(
404
+ self._workflow_event_outer_queue.put(
398
405
  self._stream_workflow_event(
399
406
  BaseOutput(
400
407
  name=workflow_output_descriptor.name,
@@ -414,7 +421,7 @@ class WorkflowRunner(Generic[StateType]):
414
421
 
415
422
  return None
416
423
 
417
- raise ValueError(f"Invalid event name: {event.name}")
424
+ return None
418
425
 
419
426
  def _initiate_workflow_event(self) -> WorkflowExecutionInitiatedEvent:
420
427
  return WorkflowExecutionInitiatedEvent(
@@ -435,7 +442,7 @@ class WorkflowRunner(Generic[StateType]):
435
442
  workflow_definition=self.workflow.__class__,
436
443
  output=output,
437
444
  ),
438
- parent=self._parent_context
445
+ parent=self._parent_context,
439
446
  )
440
447
 
441
448
  def _fulfill_workflow_event(self, outputs: OutputsType) -> WorkflowExecutionFulfilledEvent:
@@ -484,9 +491,12 @@ class WorkflowRunner(Generic[StateType]):
484
491
  # TODO: We should likely handle this during initialization
485
492
  # https://app.shortcut.com/vellum/story/4327
486
493
  if not self._entrypoints:
487
- self._workflow_event_queue.put(
494
+ self._workflow_event_outer_queue.put(
488
495
  self._reject_workflow_event(
489
- VellumError(message="No entrypoints defined", code=VellumErrorCode.INVALID_WORKFLOW)
496
+ VellumError(
497
+ message="No entrypoints defined",
498
+ code=VellumErrorCode.INVALID_WORKFLOW,
499
+ )
490
500
  )
491
501
  )
492
502
  return
@@ -498,12 +508,12 @@ class WorkflowRunner(Generic[StateType]):
498
508
  try:
499
509
  self._run_node_if_ready(self._initial_state, node_cls)
500
510
  except NodeException as e:
501
- self._workflow_event_queue.put(self._reject_workflow_event(e.error))
511
+ self._workflow_event_outer_queue.put(self._reject_workflow_event(e.error))
502
512
  return
503
513
  except Exception:
504
514
  err_message = f"An unexpected error occurred while initializing node {node_cls.__name__}"
505
515
  logger.exception(err_message)
506
- self._workflow_event_queue.put(
516
+ self._workflow_event_outer_queue.put(
507
517
  self._reject_workflow_event(
508
518
  VellumError(code=VellumErrorCode.INTERNAL_ERROR, message=err_message),
509
519
  )
@@ -516,21 +526,20 @@ class WorkflowRunner(Generic[StateType]):
516
526
  if not self._active_nodes_by_execution_id:
517
527
  break
518
528
 
519
- work_item_event = self._work_item_event_queue.get()
520
- event = work_item_event.event
529
+ event = self._workflow_event_inner_queue.get()
521
530
 
522
- self._workflow_event_queue.put(event)
531
+ self._workflow_event_outer_queue.put(event)
523
532
 
524
- rejection_error = self._handle_work_item_event(work_item_event)
533
+ rejection_error = self._handle_work_item_event(event)
525
534
  if rejection_error:
526
535
  break
527
536
 
528
537
  # Handle any remaining events
529
538
  try:
530
- while work_item_event := self._work_item_event_queue.get_nowait():
531
- self._workflow_event_queue.put(work_item_event.event)
539
+ while event := self._workflow_event_inner_queue.get_nowait():
540
+ self._workflow_event_outer_queue.put(event)
532
541
 
533
- rejection_error = self._handle_work_item_event(work_item_event)
542
+ rejection_error = self._handle_work_item_event(event)
534
543
  if rejection_error:
535
544
  break
536
545
  except Empty:
@@ -546,14 +555,13 @@ class WorkflowRunner(Generic[StateType]):
546
555
  if node_input_value is UNDEF
547
556
  }
548
557
  if unresolved_external_inputs:
549
- self._workflow_event_queue.put(
558
+ self._workflow_event_outer_queue.put(
550
559
  self._pause_workflow_event(unresolved_external_inputs),
551
560
  )
552
561
  return
553
562
 
554
- final_state.meta.is_terminated = True
555
563
  if rejection_error:
556
- self._workflow_event_queue.put(self._reject_workflow_event(rejection_error))
564
+ self._workflow_event_outer_queue.put(self._reject_workflow_event(rejection_error))
557
565
  return
558
566
 
559
567
  fulfilled_outputs = self.workflow.Outputs()
@@ -561,9 +569,13 @@ class WorkflowRunner(Generic[StateType]):
561
569
  if isinstance(value, BaseDescriptor):
562
570
  setattr(fulfilled_outputs, descriptor.name, value.resolve(final_state))
563
571
  elif isinstance(descriptor.instance, BaseDescriptor):
564
- setattr(fulfilled_outputs, descriptor.name, descriptor.instance.resolve(final_state))
572
+ setattr(
573
+ fulfilled_outputs,
574
+ descriptor.name,
575
+ descriptor.instance.resolve(final_state),
576
+ )
565
577
 
566
- self._workflow_event_queue.put(self._fulfill_workflow_event(fulfilled_outputs))
578
+ self._workflow_event_outer_queue.put(self._fulfill_workflow_event(fulfilled_outputs))
567
579
 
568
580
  def _run_background_thread(self) -> None:
569
581
  state_class = self.workflow.get_state_class()
@@ -584,55 +596,71 @@ class WorkflowRunner(Generic[StateType]):
584
596
  return
585
597
 
586
598
  self._cancel_signal.wait()
587
- self._workflow_event_queue.put(
599
+ self._workflow_event_outer_queue.put(
588
600
  self._reject_workflow_event(
589
- VellumError(code=VellumErrorCode.WORKFLOW_CANCELLED, message="Workflow run cancelled")
601
+ VellumError(
602
+ code=VellumErrorCode.WORKFLOW_CANCELLED,
603
+ message="Workflow run cancelled",
604
+ )
590
605
  )
591
606
  )
592
607
 
608
+ def _is_terminal_event(self, event: WorkflowEvent) -> bool:
609
+ if (
610
+ event.name == "workflow.execution.fulfilled"
611
+ or event.name == "workflow.execution.rejected"
612
+ or event.name == "workflow.execution.paused"
613
+ ):
614
+ return event.workflow_definition == self.workflow.__class__
615
+ return False
616
+
593
617
  def stream(self) -> WorkflowEventStream:
594
618
  background_thread = Thread(
595
- target=self._run_background_thread, name=f"{self.workflow.__class__.__name__}.background_thread"
619
+ target=self._run_background_thread,
620
+ name=f"{self.workflow.__class__.__name__}.background_thread",
596
621
  )
597
622
  background_thread.start()
598
623
 
599
624
  if self._cancel_signal:
600
625
  cancel_thread = Thread(
601
- target=self._run_cancel_thread, name=f"{self.workflow.__class__.__name__}.cancel_thread"
626
+ target=self._run_cancel_thread,
627
+ name=f"{self.workflow.__class__.__name__}.cancel_thread",
602
628
  )
603
629
  cancel_thread.start()
604
630
 
605
631
  event: WorkflowEvent
606
- if self._initial_state.meta.is_terminated or self._initial_state.meta.is_terminated is None:
607
- event = self._initiate_workflow_event()
608
- else:
632
+ if self._is_resuming:
609
633
  event = self._resume_workflow_event()
634
+ else:
635
+ event = self._initiate_workflow_event()
610
636
 
611
637
  yield self._emit_event(event)
612
- self._initial_state.meta.is_terminated = False
613
638
 
614
639
  # The extra level of indirection prevents the runner from waiting on the caller to consume the event stream
615
- stream_thread = Thread(target=self._stream, name=f"{self.workflow.__class__.__name__}.stream_thread")
640
+ stream_thread = Thread(
641
+ target=self._stream,
642
+ name=f"{self.workflow.__class__.__name__}.stream_thread",
643
+ )
616
644
  stream_thread.start()
617
645
 
618
646
  while stream_thread.is_alive():
619
647
  try:
620
- event = self._workflow_event_queue.get(timeout=0.1)
648
+ event = self._workflow_event_outer_queue.get(timeout=0.1)
621
649
  except Empty:
622
650
  continue
623
651
 
624
652
  yield self._emit_event(event)
625
653
 
626
- if is_terminal_event(event):
654
+ if self._is_terminal_event(event):
627
655
  break
628
656
 
629
657
  try:
630
- while event := self._workflow_event_queue.get_nowait():
658
+ while event := self._workflow_event_outer_queue.get_nowait():
631
659
  yield self._emit_event(event)
632
660
  except Empty:
633
661
  pass
634
662
 
635
- if not is_terminal_event(event):
663
+ if not self._is_terminal_event(event):
636
664
  yield self._reject_workflow_event(
637
665
  VellumError(
638
666
  code=VellumErrorCode.INTERNAL_ERROR,