vellum-workflow-server 1.11.0.post1__tar.gz → 1.12.0.post1__tar.gz

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.
Files changed (36) hide show
  1. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/PKG-INFO +5 -3
  2. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/README.md +3 -1
  3. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/pyproject.toml +2 -2
  4. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/api/tests/test_workflow_view.py +9 -1
  5. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/api/tests/test_workflow_view_stream_workflow_route.py +77 -1
  6. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/api/workflow_view.py +94 -8
  7. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/core/cancel_workflow.py +11 -7
  8. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/core/executor.py +3 -0
  9. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/core/workflow_executor_context.py +1 -0
  10. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/utils/utils.py +9 -0
  11. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/__init__.py +0 -0
  12. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/api/__init__.py +0 -0
  13. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/api/auth_middleware.py +0 -0
  14. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/api/healthz_view.py +0 -0
  15. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/api/status_view.py +0 -0
  16. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/api/tests/__init__.py +0 -0
  17. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/api/tests/test_input_display_mapping.py +0 -0
  18. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/api/tests/test_workflow_view_async_exec.py +0 -0
  19. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/code_exec_runner.py +0 -0
  20. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/config.py +0 -0
  21. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/core/__init__.py +0 -0
  22. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/core/events.py +0 -0
  23. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/core/utils.py +0 -0
  24. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/logging_config.py +0 -0
  25. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/server.py +0 -0
  26. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/start.py +0 -0
  27. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/utils/__init__.py +0 -0
  28. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/utils/exit_handler.py +0 -0
  29. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/utils/log_proxy.py +0 -0
  30. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/utils/oom_killer.py +0 -0
  31. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/utils/sentry.py +0 -0
  32. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/utils/system_utils.py +0 -0
  33. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/utils/tests/__init__.py +0 -0
  34. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/utils/tests/test_sentry_integration.py +0 -0
  35. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/utils/tests/test_system_utils.py +0 -0
  36. {vellum_workflow_server-1.11.0.post1 → vellum_workflow_server-1.12.0.post1}/src/workflow_server/utils/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: vellum-workflow-server
3
- Version: 1.11.0.post1
3
+ Version: 1.12.0.post1
4
4
  Summary:
5
5
  License: AGPL
6
6
  Requires-Python: >=3.9.0,<4
@@ -30,13 +30,15 @@ Requires-Dist: pyjwt (==2.10.0)
30
30
  Requires-Dist: python-dotenv (==1.2.1)
31
31
  Requires-Dist: retrying (==1.3.4)
32
32
  Requires-Dist: sentry-sdk[flask] (==2.20.0)
33
- Requires-Dist: vellum-ai (==1.11.0)
33
+ Requires-Dist: vellum-ai (==1.12.0)
34
34
  Description-Content-Type: text/markdown
35
35
 
36
36
  # Vellum Workflow Runner Server
37
+
37
38
  This package is meant for installing on container images in order to use custom docker images when using Vellum Workflows.
38
39
 
39
40
  ## Example Dockerfile Usage:
41
+
40
42
  ```
41
43
  FROM python:3.11.6-slim-bookworm
42
44
 
@@ -49,7 +51,6 @@ RUN pip install --upgrade pip
49
51
  RUN pip --no-cache-dir install vellum-workflow-server==0.13.2
50
52
 
51
53
  ENV PYTHONUNBUFFERED 1
52
- ENV PYTHONDONTWRITEBYTECODE 1
53
54
  COPY ./base-image/code_exec_entrypoint.sh .
54
55
  RUN chmod +x /code_exec_entrypoint.sh
55
56
 
@@ -57,5 +58,6 @@ CMD ["vellum_start_server"]
57
58
  ```
58
59
 
59
60
  ## Skipping Publishes
61
+
60
62
  If you wish to automatically skip publishing a new version when merging to main you can add a [skip-publish] to your commit message. This is useful if your changes are not time sensitive and can just go out with the next release. This avoids causing new services being created causing extra cold starts for our customers and also keeps our public versioning more tidy.
61
63
 
@@ -1,7 +1,9 @@
1
1
  # Vellum Workflow Runner Server
2
+
2
3
  This package is meant for installing on container images in order to use custom docker images when using Vellum Workflows.
3
4
 
4
5
  ## Example Dockerfile Usage:
6
+
5
7
  ```
6
8
  FROM python:3.11.6-slim-bookworm
7
9
 
@@ -14,7 +16,6 @@ RUN pip install --upgrade pip
14
16
  RUN pip --no-cache-dir install vellum-workflow-server==0.13.2
15
17
 
16
18
  ENV PYTHONUNBUFFERED 1
17
- ENV PYTHONDONTWRITEBYTECODE 1
18
19
  COPY ./base-image/code_exec_entrypoint.sh .
19
20
  RUN chmod +x /code_exec_entrypoint.sh
20
21
 
@@ -22,4 +23,5 @@ CMD ["vellum_start_server"]
22
23
  ```
23
24
 
24
25
  ## Skipping Publishes
26
+
25
27
  If you wish to automatically skip publishing a new version when merging to main you can add a [skip-publish] to your commit message. This is useful if your changes are not time sensitive and can just go out with the next release. This avoids causing new services being created causing extra cold starts for our customers and also keeps our public versioning more tidy.
@@ -3,7 +3,7 @@ name = "vellum-workflow-server"
3
3
 
4
4
  [tool.poetry]
5
5
  name = "vellum-workflow-server"
6
- version = "1.11.0.post1"
6
+ version = "1.12.0.post1"
7
7
  description = ""
8
8
  readme = "README.md"
9
9
  authors = []
@@ -46,7 +46,7 @@ orderly-set = "5.2.2"
46
46
  pebble = "5.0.7"
47
47
  gunicorn = "23.0.0"
48
48
  orjson = "3.11.4"
49
- vellum-ai = "1.11.0"
49
+ vellum-ai = "1.12.0"
50
50
  python-dotenv = "1.2.1"
51
51
  retrying = "1.3.4"
52
52
  sentry-sdk = {extras = ["flask"], version = "2.20.0"}
@@ -389,7 +389,15 @@ class MyAdditionNode(BaseNode):
389
389
  },
390
390
  "id": "2464b610-fb6d-495b-b17c-933ee147f19f",
391
391
  "label": "My Addition Node",
392
- "outputs": [{"id": "f39d85c9-e7bf-45e1-bb67-f16225db0118", "name": "result", "type": "NUMBER", "value": None}],
392
+ "outputs": [
393
+ {
394
+ "id": "f39d85c9-e7bf-45e1-bb67-f16225db0118",
395
+ "name": "result",
396
+ "type": "NUMBER",
397
+ "value": None,
398
+ "schema": {"type": "integer"},
399
+ }
400
+ ],
393
401
  "ports": [{"id": "bc489295-cd8a-4aa2-88bb-34446374100d", "name": "default", "type": "DEFAULT"}],
394
402
  "trigger": {"id": "ff580cad-73d6-44fe-8f2c-4b8dc990ee70", "merge_behavior": "AWAIT_ATTRIBUTES"},
395
403
  "type": "GENERIC",
@@ -549,7 +549,10 @@ class Inputs(BaseInputs):
549
549
  # AND the third event should be workflow execution rejected
550
550
  assert events[2]["name"] == "workflow.execution.rejected"
551
551
  assert events[1]["span_id"] == events[2]["span_id"]
552
- assert "Required input variables foo should have defined value" in events[2]["body"]["error"]["message"]
552
+ actual_error_message = events[2]["body"]["error"]["message"]
553
+ assert "Required input variables" in actual_error_message
554
+ assert "foo" in actual_error_message
555
+ assert "should have defined value" in actual_error_message
553
556
 
554
557
  # AND the fourth event should be vembda execution fulfilled
555
558
  assert events[3]["name"] == "vembda.execution.fulfilled"
@@ -1310,3 +1313,76 @@ class OOMWorkflow(BaseWorkflow):
1310
1313
  assert (
1311
1314
  vembda_fulfilled_event["body"].get("timed_out") is not True
1312
1315
  ), "timed_out flag should not be set when OOM occurs"
1316
+
1317
+
1318
+ @mock.patch("workflow_server.api.workflow_view.ENABLE_PROCESS_WRAPPER", False)
1319
+ def test_stream_workflow_route__client_disconnect_emits_rejected_event():
1320
+ """
1321
+ Tests that when a client disconnects mid-stream (GeneratorExit), we emit a workflow execution
1322
+ rejected event to the events.create API.
1323
+ """
1324
+ # GIVEN a valid request body for a workflow that yields multiple events
1325
+ span_id = uuid4()
1326
+ trace_id = uuid4()
1327
+ request_body = {
1328
+ "timeout": 360,
1329
+ "execution_id": str(span_id),
1330
+ "execution_context": {
1331
+ "trace_id": str(trace_id),
1332
+ },
1333
+ "inputs": [],
1334
+ "environment_api_key": "test",
1335
+ "module": "workflow",
1336
+ "files": {
1337
+ "__init__.py": "",
1338
+ "workflow.py": """\
1339
+ from vellum.workflows import BaseWorkflow
1340
+
1341
+ class Workflow(BaseWorkflow):
1342
+ class Outputs(BaseWorkflow.Outputs):
1343
+ foo = "hello"
1344
+ """,
1345
+ },
1346
+ }
1347
+
1348
+ # AND a mock to capture events.create calls
1349
+ events_create_calls = []
1350
+
1351
+ def mock_events_create(request):
1352
+ events_create_calls.append(request)
1353
+
1354
+ # WHEN we call the stream route and simulate a client disconnect
1355
+ flask_app = create_app()
1356
+ with flask_app.test_client() as test_client:
1357
+ with mock.patch("workflow_server.core.workflow_executor_context.create_vellum_client") as mock_create_client:
1358
+ mock_client = mock.MagicMock()
1359
+ mock_client.events.create = mock_events_create
1360
+ mock_create_client.return_value = mock_client
1361
+
1362
+ response = test_client.post("/workflow/stream", json=request_body)
1363
+
1364
+ # Get the response iterator and consume a few chunks to start the stream
1365
+ response_iter = response.response
1366
+ next(response_iter)
1367
+
1368
+ # Close the response to trigger GeneratorExit
1369
+ response_iter.close()
1370
+
1371
+ # THEN the events.create API should have been called with rejected event
1372
+ assert len(events_create_calls) > 0, "events.create should have been called on client disconnect"
1373
+
1374
+ # AND the call should include a workflow.execution.rejected event (sent as SDK event model)
1375
+ last_call = events_create_calls[-1]
1376
+ assert isinstance(last_call, list), "events.create should be called with a list"
1377
+ assert len(last_call) == 1, "Should have exactly one rejected event"
1378
+
1379
+ rejected_event = last_call[0]
1380
+ assert rejected_event.name == "workflow.execution.rejected", "Should be a rejected event"
1381
+
1382
+ # AND the rejected event should have the correct error message
1383
+ assert "client disconnected" in rejected_event.body.error.message.lower()
1384
+
1385
+ # AND the rejected event should have a workflow_definition
1386
+ # TODO: In the future, we should capture the real workflow_definition from the initiated event.
1387
+ # For now, we use BaseWorkflow as a placeholder.
1388
+ assert rejected_event.body.workflow_definition is not None, "Should have a workflow_definition"
@@ -22,6 +22,14 @@ from vellum_ee.workflows.display.types import WorkflowDisplayContext
22
22
  from vellum_ee.workflows.display.workflows import BaseWorkflowDisplay
23
23
  from vellum_ee.workflows.server.virtual_file_loader import VirtualFileFinder
24
24
 
25
+ from vellum.workflows import BaseWorkflow
26
+ from vellum.workflows.errors import WorkflowError, WorkflowErrorCode
27
+ from vellum.workflows.events.workflow import (
28
+ WorkflowExecutionInitiatedBody,
29
+ WorkflowExecutionInitiatedEvent,
30
+ WorkflowExecutionRejectedBody,
31
+ WorkflowExecutionRejectedEvent,
32
+ )
25
33
  from vellum.workflows.exceptions import WorkflowInitializationException
26
34
  from vellum.workflows.nodes import BaseNode
27
35
  from vellum.workflows.vellum_client import create_vellum_client
@@ -135,7 +143,7 @@ def stream_workflow_route() -> Response:
135
143
  # These can happen either from Vembda disconnects (possibily from predict disconnects) or
136
144
  # from knative activator gateway timeouts which are caused by idleTimeout or responseStartSeconds
137
145
  # being exceeded.
138
- app.logger.error(
146
+ app.logger.warning(
139
147
  "Client disconnected in the middle of the Workflow Stream",
140
148
  extra={
141
149
  "sentry_tags": {
@@ -144,6 +152,11 @@ def stream_workflow_route() -> Response:
144
152
  }
145
153
  },
146
154
  )
155
+ _emit_client_disconnect_events(
156
+ context,
157
+ span_id,
158
+ "Client disconnected in the middle of the Workflow Stream",
159
+ )
147
160
  return
148
161
  except Exception as e:
149
162
  logger.exception("Error during workflow response stream generator", extra={"error": e})
@@ -174,6 +187,75 @@ def stream_workflow_route() -> Response:
174
187
  return resp
175
188
 
176
189
 
190
+ def _emit_async_error_events(
191
+ context: WorkflowExecutorContext, error_message: str, stacktrace: Optional[str] = None
192
+ ) -> None:
193
+ """
194
+ Emit workflow execution error events when async execution fails before or during workflow startup.
195
+
196
+ This ensures that errors in async mode are properly reported to Vellum's events API,
197
+ making them visible in the executions UI.
198
+ """
199
+ try:
200
+ workflow_span_id = context.workflow_span_id or str(uuid4())
201
+
202
+ initiated_event = WorkflowExecutionInitiatedEvent[Any, Any](
203
+ trace_id=context.trace_id,
204
+ span_id=workflow_span_id,
205
+ body=WorkflowExecutionInitiatedBody(inputs=context.inputs),
206
+ parent=context.execution_context.parent_context if context.execution_context else None,
207
+ )
208
+
209
+ rejected_event = WorkflowExecutionRejectedEvent(
210
+ trace_id=context.trace_id,
211
+ span_id=workflow_span_id,
212
+ body=WorkflowExecutionRejectedBody(
213
+ error=WorkflowError(
214
+ message=error_message,
215
+ code=WorkflowErrorCode.INTERNAL_ERROR,
216
+ ),
217
+ stacktrace=stacktrace,
218
+ ),
219
+ parent=context.execution_context.parent_context if context.execution_context else None,
220
+ )
221
+
222
+ context.vellum_client.events.create(request=[initiated_event, rejected_event]) # type: ignore[list-item]
223
+ except Exception as e:
224
+ logger.exception(f"Failed to emit async error events: {e}")
225
+
226
+
227
+ def _emit_client_disconnect_events(
228
+ context: WorkflowExecutorContext,
229
+ workflow_span_id: str,
230
+ error_message: str,
231
+ ) -> None:
232
+ """
233
+ Emit workflow execution rejected event when a client disconnects mid-stream.
234
+
235
+ Since the workflow has already started streaming (the initiated event was already emitted),
236
+ we only need to emit the rejected event to properly close out the execution.
237
+ """
238
+ try:
239
+ # TODO: In the future, we should capture the real workflow_definition from the initiated event
240
+ # For now, we use BaseWorkflow as a placeholder
241
+ rejected_event = WorkflowExecutionRejectedEvent(
242
+ trace_id=context.trace_id,
243
+ span_id=workflow_span_id,
244
+ body=WorkflowExecutionRejectedBody(
245
+ workflow_definition=BaseWorkflow,
246
+ error=WorkflowError(
247
+ message=error_message,
248
+ code=WorkflowErrorCode.WORKFLOW_CANCELLED,
249
+ ),
250
+ ),
251
+ parent=context.execution_context.parent_context if context.execution_context else None,
252
+ )
253
+
254
+ context.vellum_client.events.create(request=[rejected_event]) # type: ignore[list-item]
255
+ except Exception as e:
256
+ logger.exception(f"Failed to emit client disconnect events: {e}")
257
+
258
+
177
259
  @bp.route("/async-exec", methods=["POST"])
178
260
  def async_exec_workflow() -> Response:
179
261
  data = request.get_json()
@@ -208,8 +290,8 @@ def async_exec_workflow() -> Response:
208
290
  try:
209
291
  start_workflow_result = _start_workflow(context)
210
292
  if isinstance(start_workflow_result, Response):
211
- # TODO same here, should return this response as en event or it will get yeeted to the nether
212
- # return start_workflow_result
293
+ error_detail = start_workflow_result.get_json().get("detail", "Unknown error during workflow startup")
294
+ _emit_async_error_events(context, error_detail)
213
295
  return
214
296
 
215
297
  workflow_events, vembda_initiated_event, process, span_id, headers = start_workflow_result
@@ -223,6 +305,7 @@ def async_exec_workflow() -> Response:
223
305
  )
224
306
  except Exception as e:
225
307
  logger.exception("Error during workflow async background worker", e)
308
+ _emit_async_error_events(context, str(e), traceback.format_exc())
226
309
  finally:
227
310
  if ENABLE_PROCESS_WRAPPER:
228
311
  try:
@@ -531,11 +614,18 @@ def serialize_route() -> Response:
531
614
  is_new_server = data.get("is_new_server", False)
532
615
  module = data.get("module")
533
616
 
617
+ headers = {
618
+ "X-Vellum-Is-New-Server": str(is_new_server).lower(),
619
+ }
620
+
534
621
  if not files:
622
+ error_message = "No files received"
623
+ logger.warning(error_message)
535
624
  return Response(
536
- json.dumps({"detail": "No files received"}),
625
+ json.dumps({"detail": error_message}),
537
626
  status=400,
538
627
  content_type="application/json",
628
+ headers=headers,
539
629
  )
540
630
 
541
631
  client = create_vellum_client(api_key=workspace_api_key)
@@ -544,10 +634,6 @@ def serialize_route() -> Response:
544
634
  namespace = get_random_namespace()
545
635
  virtual_finder = VirtualFileFinder(files, namespace, source_module=module)
546
636
 
547
- headers = {
548
- "X-Vellum-Is-New-Server": str(is_new_server).lower(),
549
- }
550
-
551
637
  try:
552
638
  sys.meta_path.append(virtual_finder)
553
639
  result = BaseWorkflowDisplay.serialize_module(namespace, client=client, dry_run=True)
@@ -14,14 +14,18 @@ logger = logging.getLogger(__name__)
14
14
 
15
15
 
16
16
  def get_is_workflow_cancelled(execution_id: UUID, vembda_public_url: Optional[str]) -> bool:
17
- response = requests.get(
18
- f"{vembda_public_url}/vembda-public/cancel-workflow-execution-status/{execution_id}",
19
- headers={"Accept": "application/json"},
20
- timeout=5,
21
- )
22
- response.raise_for_status()
17
+ try:
18
+ response = requests.get(
19
+ f"{vembda_public_url}/vembda-public/cancel-workflow-execution-status/{execution_id}",
20
+ headers={"Accept": "application/json"},
21
+ timeout=5,
22
+ )
23
+ response.raise_for_status()
23
24
 
24
- return response.json().get("cancelled")
25
+ return response.json().get("cancelled", False)
26
+ except Exception:
27
+ logger.exception("Error checking workflow cancellation status")
28
+ return False
25
29
 
26
30
 
27
31
  class CancelWorkflowWatcherThread(Thread):
@@ -14,6 +14,7 @@ from typing import Any, Callable, Generator, Iterator, Optional, Tuple
14
14
 
15
15
  import orjson
16
16
  from vellum_ee.workflows.display.utils.events import event_enricher
17
+ from vellum_ee.workflows.display.utils.expressions import base_descriptor_validator
17
18
  from vellum_ee.workflows.server.virtual_file_loader import VirtualFileFinder
18
19
 
19
20
  from vellum.workflows import BaseWorkflow
@@ -177,6 +178,7 @@ def stream_workflow(
177
178
  node_output_mocks = MockNodeExecution.validate_all(
178
179
  executor_context.node_output_mocks,
179
180
  workflow.__class__,
181
+ descriptor_validator=base_descriptor_validator,
180
182
  )
181
183
 
182
184
  cancel_signal = cancel_signal or ThreadingEvent()
@@ -192,6 +194,7 @@ def stream_workflow(
192
194
  timeout=executor_context.timeout,
193
195
  trigger=trigger,
194
196
  execution_id=executor_context.workflow_span_id,
197
+ event_max_size=executor_context.event_max_size,
195
198
  )
196
199
  except WorkflowInitializationException as e:
197
200
  cancel_watcher_kill_switch.set()
@@ -41,6 +41,7 @@ class BaseExecutorContext(UniversalBaseModel):
41
41
  # when running in async mode.
42
42
  workflow_span_id: Optional[UUID] = None
43
43
  vembda_service_initiated_timestamp: Optional[int] = None
44
+ event_max_size: Optional[int] = None
44
45
 
45
46
  @field_validator("inputs", mode="before")
46
47
  @classmethod
@@ -59,10 +59,19 @@ def convert_json_inputs_to_vellum(inputs: List[dict]) -> dict:
59
59
 
60
60
 
61
61
  def get_version() -> dict:
62
+ # Return hotswappable lock file so we can save it and reuse it
63
+ lock_file = None
64
+ try:
65
+ with open("/app/uv.lock", "r") as f:
66
+ lock_file = f.read()
67
+ except Exception:
68
+ pass
69
+
62
70
  return {
63
71
  "sdk_version": version("vellum-ai"),
64
72
  "server_version": "local" if is_development() else version("vellum-workflow-server"),
65
73
  "container_image": CONTAINER_IMAGE,
74
+ "lock_file": lock_file,
66
75
  }
67
76
 
68
77