griptape-nodes 0.51.1__py3-none-any.whl → 0.52.0__py3-none-any.whl

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 (46) hide show
  1. griptape_nodes/__init__.py +5 -4
  2. griptape_nodes/app/api.py +27 -24
  3. griptape_nodes/app/app.py +243 -221
  4. griptape_nodes/app/watch.py +17 -2
  5. griptape_nodes/bootstrap/workflow_executors/local_workflow_executor.py +66 -103
  6. griptape_nodes/bootstrap/workflow_executors/workflow_executor.py +16 -4
  7. griptape_nodes/exe_types/core_types.py +16 -4
  8. griptape_nodes/exe_types/node_types.py +74 -16
  9. griptape_nodes/machines/control_flow.py +21 -26
  10. griptape_nodes/machines/fsm.py +16 -16
  11. griptape_nodes/machines/node_resolution.py +28 -119
  12. griptape_nodes/mcp_server/server.py +14 -10
  13. griptape_nodes/mcp_server/ws_request_manager.py +2 -2
  14. griptape_nodes/node_library/workflow_registry.py +5 -0
  15. griptape_nodes/retained_mode/events/base_events.py +12 -7
  16. griptape_nodes/retained_mode/events/execution_events.py +0 -6
  17. griptape_nodes/retained_mode/events/node_events.py +38 -0
  18. griptape_nodes/retained_mode/events/parameter_events.py +11 -0
  19. griptape_nodes/retained_mode/events/variable_events.py +361 -0
  20. griptape_nodes/retained_mode/events/workflow_events.py +35 -0
  21. griptape_nodes/retained_mode/griptape_nodes.py +61 -26
  22. griptape_nodes/retained_mode/managers/agent_manager.py +8 -9
  23. griptape_nodes/retained_mode/managers/event_manager.py +215 -74
  24. griptape_nodes/retained_mode/managers/flow_manager.py +39 -33
  25. griptape_nodes/retained_mode/managers/library_lifecycle/library_directory.py +14 -14
  26. griptape_nodes/retained_mode/managers/library_lifecycle/library_fsm.py +20 -20
  27. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/base.py +1 -1
  28. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/github.py +1 -1
  29. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/local_file.py +4 -3
  30. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/package.py +1 -1
  31. griptape_nodes/retained_mode/managers/library_lifecycle/library_provenance/sandbox.py +1 -1
  32. griptape_nodes/retained_mode/managers/library_manager.py +20 -19
  33. griptape_nodes/retained_mode/managers/node_manager.py +81 -8
  34. griptape_nodes/retained_mode/managers/object_manager.py +4 -0
  35. griptape_nodes/retained_mode/managers/settings.py +1 -0
  36. griptape_nodes/retained_mode/managers/sync_manager.py +3 -9
  37. griptape_nodes/retained_mode/managers/variable_manager.py +529 -0
  38. griptape_nodes/retained_mode/managers/workflow_manager.py +156 -50
  39. griptape_nodes/retained_mode/variable_types.py +18 -0
  40. griptape_nodes/utils/__init__.py +4 -0
  41. griptape_nodes/utils/async_utils.py +89 -0
  42. {griptape_nodes-0.51.1.dist-info → griptape_nodes-0.52.0.dist-info}/METADATA +2 -3
  43. {griptape_nodes-0.51.1.dist-info → griptape_nodes-0.52.0.dist-info}/RECORD +45 -42
  44. {griptape_nodes-0.51.1.dist-info → griptape_nodes-0.52.0.dist-info}/WHEEL +1 -1
  45. griptape_nodes/bootstrap/workflow_executors/subprocess_workflow_executor.py +0 -90
  46. {griptape_nodes-0.51.1.dist-info → griptape_nodes-0.52.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,529 @@
1
+ import logging
2
+ from typing import NamedTuple
3
+
4
+ from griptape_nodes.retained_mode.events.base_events import ResultPayload
5
+ from griptape_nodes.retained_mode.events.variable_events import (
6
+ CreateVariableRequest,
7
+ CreateVariableResultFailure,
8
+ CreateVariableResultSuccess,
9
+ DeleteVariableRequest,
10
+ DeleteVariableResultFailure,
11
+ DeleteVariableResultSuccess,
12
+ GetVariableDetailsRequest,
13
+ GetVariableDetailsResultFailure,
14
+ GetVariableDetailsResultSuccess,
15
+ GetVariableRequest,
16
+ GetVariableResultFailure,
17
+ GetVariableResultSuccess,
18
+ GetVariableTypeRequest,
19
+ GetVariableTypeResultFailure,
20
+ GetVariableTypeResultSuccess,
21
+ GetVariableValueRequest,
22
+ GetVariableValueResultFailure,
23
+ GetVariableValueResultSuccess,
24
+ HasVariableRequest,
25
+ HasVariableResultFailure,
26
+ HasVariableResultSuccess,
27
+ ListVariablesRequest,
28
+ ListVariablesResultFailure,
29
+ ListVariablesResultSuccess,
30
+ RenameVariableRequest,
31
+ RenameVariableResultFailure,
32
+ RenameVariableResultSuccess,
33
+ SetVariableTypeRequest,
34
+ SetVariableTypeResultFailure,
35
+ SetVariableTypeResultSuccess,
36
+ SetVariableValueRequest,
37
+ SetVariableValueResultFailure,
38
+ SetVariableValueResultSuccess,
39
+ VariableDetails,
40
+ )
41
+ from griptape_nodes.retained_mode.griptape_nodes import GriptapeNodes
42
+ from griptape_nodes.retained_mode.managers.event_manager import EventManager
43
+ from griptape_nodes.retained_mode.variable_types import FlowVariable, VariableScope
44
+
45
+ logger = logging.getLogger("griptape_nodes")
46
+
47
+
48
+ class VariableLookupResult(NamedTuple):
49
+ """Result of hierarchical variable lookup."""
50
+
51
+ variable: FlowVariable | None
52
+ found_scope: VariableScope | None
53
+
54
+
55
+ class VariablesManager:
56
+ """Manager for variables with scoped access control."""
57
+
58
+ def __init__(self, event_manager: EventManager | None = None) -> None:
59
+ # Storage for flow-scoped variables: {flow_name: {variable_name: FlowVariable}}
60
+ self._flow_variables: dict[str, dict[str, FlowVariable]] = {}
61
+ # Storage for global variables: {variable_name: FlowVariable}
62
+ self._global_variables: dict[str, FlowVariable] = {}
63
+ if event_manager is not None:
64
+ event_manager.assign_manager_to_request_type(CreateVariableRequest, self.on_create_variable_request)
65
+ event_manager.assign_manager_to_request_type(GetVariableRequest, self.on_get_variable_request)
66
+ event_manager.assign_manager_to_request_type(GetVariableValueRequest, self.on_get_variable_value_request)
67
+ event_manager.assign_manager_to_request_type(SetVariableValueRequest, self.on_set_variable_value_request)
68
+ event_manager.assign_manager_to_request_type(GetVariableTypeRequest, self.on_get_variable_type_request)
69
+ event_manager.assign_manager_to_request_type(SetVariableTypeRequest, self.on_set_variable_type_request)
70
+ event_manager.assign_manager_to_request_type(DeleteVariableRequest, self.on_delete_variable_request)
71
+ event_manager.assign_manager_to_request_type(RenameVariableRequest, self.on_rename_variable_request)
72
+ event_manager.assign_manager_to_request_type(HasVariableRequest, self.on_has_variable_request)
73
+ event_manager.assign_manager_to_request_type(ListVariablesRequest, self.on_list_variables_request)
74
+ event_manager.assign_manager_to_request_type(
75
+ GetVariableDetailsRequest, self.on_get_variable_details_request
76
+ )
77
+
78
+ def on_clear_object_state(self) -> None:
79
+ """Clear all variables."""
80
+ self._flow_variables.clear()
81
+ self._global_variables.clear()
82
+
83
+ def _get_starting_flow(self, starting_flow: str | None) -> str:
84
+ """Get the starting flow name, using Context Manager if None."""
85
+ if starting_flow is not None:
86
+ # Validate that the specified flow exists
87
+ flow_manager = GriptapeNodes.FlowManager()
88
+ try:
89
+ flow_manager.get_parent_flow(starting_flow) # This will raise if flow doesn't exist
90
+ except Exception as e:
91
+ msg = f"Specified starting flow '{starting_flow}' does not exist: {e}"
92
+ raise ValueError(msg) from e
93
+ return starting_flow
94
+
95
+ # Get current flow from Context Manager
96
+ context_manager = GriptapeNodes.ContextManager()
97
+
98
+ if not context_manager.has_current_flow():
99
+ msg = "No starting flow specified and no current flow in Context Manager"
100
+ raise ValueError(msg)
101
+
102
+ return context_manager.get_current_flow().name
103
+
104
+ def _get_flow_hierarchy(self, starting_flow: str) -> list[str]:
105
+ """Get the flow hierarchy from starting flow up to root."""
106
+ flow_manager = GriptapeNodes.FlowManager()
107
+
108
+ hierarchy = []
109
+ current_flow = starting_flow
110
+
111
+ while current_flow:
112
+ hierarchy.append(current_flow)
113
+ try:
114
+ parent = flow_manager.get_parent_flow(current_flow)
115
+ current_flow = parent
116
+ except Exception:
117
+ # No parent flow found, we've reached the root
118
+ break
119
+
120
+ return hierarchy
121
+
122
+ def _find_variable_in_flow(self, flow_name: str, variable_name: str) -> FlowVariable | None:
123
+ """Find a variable in a specific flow."""
124
+ flow_vars = self._flow_variables.get(flow_name, {})
125
+ return flow_vars.get(variable_name)
126
+
127
+ def _find_variable_hierarchical(
128
+ self, starting_flow: str, variable_name: str, lookup_scope: VariableScope
129
+ ) -> VariableLookupResult:
130
+ """Find a variable using hierarchical lookup strategy."""
131
+ match lookup_scope:
132
+ case VariableScope.CURRENT_FLOW_ONLY:
133
+ variable = self._find_variable_in_flow(starting_flow, variable_name)
134
+ found_scope = VariableScope.CURRENT_FLOW_ONLY if variable else None
135
+ return VariableLookupResult(variable=variable, found_scope=found_scope)
136
+
137
+ case VariableScope.GLOBAL_ONLY:
138
+ variable = self._global_variables.get(variable_name)
139
+ found_scope = VariableScope.GLOBAL_ONLY if variable else None
140
+ return VariableLookupResult(variable=variable, found_scope=found_scope)
141
+
142
+ case VariableScope.HIERARCHICAL:
143
+ # Search through flow hierarchy
144
+ hierarchy = self._get_flow_hierarchy(starting_flow)
145
+ for flow_name in hierarchy:
146
+ variable = self._find_variable_in_flow(flow_name, variable_name)
147
+ if variable:
148
+ found_scope = (
149
+ VariableScope.CURRENT_FLOW_ONLY
150
+ if flow_name == starting_flow
151
+ else VariableScope.HIERARCHICAL
152
+ )
153
+ return VariableLookupResult(variable=variable, found_scope=found_scope)
154
+
155
+ # Check global variables as fallback
156
+ variable = self._global_variables.get(variable_name)
157
+ found_scope = VariableScope.GLOBAL_ONLY if variable else None
158
+ return VariableLookupResult(variable=variable, found_scope=found_scope)
159
+
160
+ case VariableScope.ALL:
161
+ # This is primarily for ListVariables - just search current flow for now
162
+ variable = self._find_variable_in_flow(starting_flow, variable_name)
163
+ found_scope = VariableScope.CURRENT_FLOW_ONLY if variable else None
164
+ return VariableLookupResult(variable=variable, found_scope=found_scope)
165
+
166
+ case _:
167
+ msg = f"Attempted to find variable '{variable_name}' from starting flow '{starting_flow}', but encountered an unknown/unexpected variable scope '{lookup_scope.value}'"
168
+ raise ValueError(msg)
169
+
170
+ def on_create_variable_request(self, request: CreateVariableRequest) -> ResultPayload:
171
+ """Create a new variable."""
172
+ if request.is_global:
173
+ # Check for name collision in global variables
174
+ if request.name in self._global_variables:
175
+ return CreateVariableResultFailure(
176
+ result_details=f"Attempted to create a global variable named '{request.name}'. Failed because a variable with that name already exists."
177
+ )
178
+
179
+ # Create global variable
180
+ variable = FlowVariable(
181
+ name=request.name,
182
+ owning_flow_name=None,
183
+ type=request.type,
184
+ value=request.value,
185
+ )
186
+
187
+ self._global_variables[request.name] = variable
188
+ return CreateVariableResultSuccess(result_details=f"Successfully created global variable '{request.name}'.")
189
+
190
+ # Get the target flow
191
+ try:
192
+ target_flow = self._get_starting_flow(request.owning_flow)
193
+ except ValueError as e:
194
+ return CreateVariableResultFailure(
195
+ result_details=f"Attempted to create variable '{request.name}'. Failed to determine target flow: {e}"
196
+ )
197
+
198
+ # Initialize flow storage if needed
199
+ if target_flow not in self._flow_variables:
200
+ self._flow_variables[target_flow] = {}
201
+
202
+ # Check for name collision in target flow
203
+ if request.name in self._flow_variables[target_flow]:
204
+ return CreateVariableResultFailure(
205
+ result_details=f"Attempted to create a variable named '{request.name}' in flow '{target_flow}'. Failed because a variable with that name already exists."
206
+ )
207
+
208
+ # Create flow-scoped variable
209
+ variable = FlowVariable(
210
+ name=request.name,
211
+ owning_flow_name=target_flow,
212
+ type=request.type,
213
+ value=request.value,
214
+ )
215
+
216
+ self._flow_variables[target_flow][request.name] = variable
217
+ return CreateVariableResultSuccess(
218
+ result_details=f"Successfully created variable '{request.name}' in flow '{target_flow}'."
219
+ )
220
+
221
+ def on_get_variable_request(self, request: GetVariableRequest) -> ResultPayload:
222
+ """Get a full variable by name."""
223
+ try:
224
+ starting_flow = self._get_starting_flow(request.starting_flow)
225
+ except ValueError as e:
226
+ return GetVariableResultFailure(
227
+ result_details=f"Attempted to get variable '{request.name}'. Failed to determine starting flow: {e}"
228
+ )
229
+
230
+ result = self._find_variable_hierarchical(starting_flow, request.name, request.lookup_scope)
231
+
232
+ if not result.variable:
233
+ return GetVariableResultFailure(
234
+ result_details=f"Attempted to get variable '{request.name}'. Failed because no such variable could be found."
235
+ )
236
+
237
+ return GetVariableResultSuccess(
238
+ variable=result.variable, result_details=f"Successfully retrieved variable '{request.name}'."
239
+ )
240
+
241
+ def on_get_variable_value_request(self, request: GetVariableValueRequest) -> ResultPayload:
242
+ """Get the value of a variable."""
243
+ try:
244
+ starting_flow = self._get_starting_flow(request.starting_flow)
245
+ except ValueError as e:
246
+ return GetVariableValueResultFailure(
247
+ result_details=f"Attempted to get value for variable '{request.name}'. Failed to determine starting flow: {e}"
248
+ )
249
+
250
+ result = self._find_variable_hierarchical(starting_flow, request.name, request.lookup_scope)
251
+
252
+ if not result.variable:
253
+ return GetVariableValueResultFailure(
254
+ result_details=f"Attempted to get value for variable '{request.name}'. Failed because no such variable could be found."
255
+ )
256
+
257
+ return GetVariableValueResultSuccess(
258
+ value=result.variable.value, result_details=f"Successfully retrieved value for variable '{request.name}'."
259
+ )
260
+
261
+ def on_set_variable_value_request(self, request: SetVariableValueRequest) -> ResultPayload:
262
+ """Set the value of an existing variable."""
263
+ try:
264
+ starting_flow = self._get_starting_flow(request.starting_flow)
265
+ except ValueError as e:
266
+ return SetVariableValueResultFailure(
267
+ result_details=f"Attempted to set value for variable '{request.name}'. Failed to determine starting flow: {e}"
268
+ )
269
+
270
+ result = self._find_variable_hierarchical(starting_flow, request.name, request.lookup_scope)
271
+
272
+ if not result.variable:
273
+ return SetVariableValueResultFailure(
274
+ result_details=f"Attempted to set value for variable '{request.name}'. Failed because no such variable could be found."
275
+ )
276
+
277
+ result.variable.value = request.value
278
+ return SetVariableValueResultSuccess(result_details=f"Successfully set value for variable '{request.name}'.")
279
+
280
+ def on_get_variable_type_request(self, request: GetVariableTypeRequest) -> ResultPayload:
281
+ """Get the type of a variable."""
282
+ try:
283
+ starting_flow = self._get_starting_flow(request.starting_flow)
284
+ except ValueError as e:
285
+ return GetVariableTypeResultFailure(
286
+ result_details=f"Attempted to get type for variable '{request.name}'. Failed to determine starting flow: {e}"
287
+ )
288
+
289
+ result = self._find_variable_hierarchical(starting_flow, request.name, request.lookup_scope)
290
+
291
+ if not result.variable:
292
+ return GetVariableTypeResultFailure(
293
+ result_details=f"Attempted to get type for variable '{request.name}'. Failed because no such variable could be found."
294
+ )
295
+
296
+ return GetVariableTypeResultSuccess(
297
+ type=result.variable.type, result_details=f"Successfully retrieved type for variable '{request.name}'."
298
+ )
299
+
300
+ def on_set_variable_type_request(self, request: SetVariableTypeRequest) -> ResultPayload:
301
+ """Set the type of an existing variable."""
302
+ try:
303
+ starting_flow = self._get_starting_flow(request.starting_flow)
304
+ except ValueError as e:
305
+ return SetVariableTypeResultFailure(
306
+ result_details=f"Attempted to set type for variable '{request.name}'. Failed to determine starting flow: {e}"
307
+ )
308
+
309
+ result = self._find_variable_hierarchical(starting_flow, request.name, request.lookup_scope)
310
+
311
+ if not result.variable:
312
+ return SetVariableTypeResultFailure(
313
+ result_details=f"Attempted to set type for variable '{request.name}'. Failed because no such variable could be found."
314
+ )
315
+
316
+ result.variable.type = request.type
317
+ return SetVariableTypeResultSuccess(
318
+ result_details=f"Successfully set type for variable '{request.name}' to '{request.type}'."
319
+ )
320
+
321
+ def on_delete_variable_request(self, request: DeleteVariableRequest) -> ResultPayload:
322
+ """Delete a variable."""
323
+ try:
324
+ starting_flow = self._get_starting_flow(request.starting_flow)
325
+ except ValueError as e:
326
+ return DeleteVariableResultFailure(
327
+ result_details=f"Attempted to delete variable '{request.name}'. Failed to determine starting flow: {e}"
328
+ )
329
+
330
+ result = self._find_variable_hierarchical(starting_flow, request.name, request.lookup_scope)
331
+
332
+ if not result.variable:
333
+ return DeleteVariableResultFailure(
334
+ result_details=f"Attempted to delete variable '{request.name}'. Failed because no such variable could be found."
335
+ )
336
+
337
+ variable = result.variable
338
+
339
+ # Remove from appropriate storage based on owning flow
340
+ if variable.owning_flow_name is None:
341
+ # Global variable
342
+ del self._global_variables[variable.name]
343
+ else:
344
+ # Flow-scoped variable
345
+ flow_vars = self._flow_variables.get(variable.owning_flow_name, {})
346
+ if variable.name in flow_vars:
347
+ del flow_vars[variable.name]
348
+
349
+ return DeleteVariableResultSuccess(result_details=f"Successfully deleted variable '{request.name}'.")
350
+
351
+ def on_rename_variable_request(self, request: RenameVariableRequest) -> ResultPayload:
352
+ """Rename a variable."""
353
+ try:
354
+ starting_flow = self._get_starting_flow(request.starting_flow)
355
+ except ValueError as e:
356
+ return RenameVariableResultFailure(
357
+ result_details=f"Attempted to rename variable '{request.name}'. Failed to determine starting flow: {e}"
358
+ )
359
+
360
+ result = self._find_variable_hierarchical(starting_flow, request.name, request.lookup_scope)
361
+
362
+ if not result.variable:
363
+ return RenameVariableResultFailure(
364
+ result_details=f"Attempted to rename variable '{request.name}'. Failed because no such variable could be found."
365
+ )
366
+
367
+ variable = result.variable
368
+
369
+ # Check for name collision with new name in the same scope
370
+ new_name_result = self._find_variable_hierarchical(starting_flow, request.new_name, request.lookup_scope)
371
+ if new_name_result.variable and new_name_result.variable.name != variable.name:
372
+ return RenameVariableResultFailure(
373
+ result_details=f"Attempted to rename variable '{request.name}' to '{request.new_name}'. Failed because a variable with that name already exists."
374
+ )
375
+
376
+ # Update the variable name and storage key
377
+ old_name = variable.name
378
+ variable.name = request.new_name
379
+
380
+ # Update in appropriate storage based on owning flow
381
+ if variable.owning_flow_name is None:
382
+ # Global variable
383
+ del self._global_variables[old_name]
384
+ self._global_variables[request.new_name] = variable
385
+ else:
386
+ # Flow-scoped variable
387
+ flow_vars = self._flow_variables.get(variable.owning_flow_name, {})
388
+ if old_name in flow_vars:
389
+ del flow_vars[old_name]
390
+ flow_vars[request.new_name] = variable
391
+
392
+ return RenameVariableResultSuccess(
393
+ result_details=f"Successfully renamed variable '{old_name}' to '{request.new_name}'."
394
+ )
395
+
396
+ def on_has_variable_request(self, request: HasVariableRequest) -> ResultPayload:
397
+ """Check if a variable exists."""
398
+ try:
399
+ starting_flow = self._get_starting_flow(request.starting_flow)
400
+ except ValueError as e:
401
+ return HasVariableResultFailure(
402
+ result_details=f"Attempted to check existence of variable '{request.name}'. Failed to determine starting flow: {e}"
403
+ )
404
+
405
+ result = self._find_variable_hierarchical(starting_flow, request.name, request.lookup_scope)
406
+ exists = result.variable is not None
407
+
408
+ return HasVariableResultSuccess(
409
+ exists=exists,
410
+ found_scope=result.found_scope,
411
+ result_details=f"Successfully checked existence of variable '{request.name}': {'exists' if exists else 'not found'}.",
412
+ )
413
+
414
+ def _get_variables_by_scope(self, starting_flow: str, lookup_scope: VariableScope) -> list[FlowVariable]:
415
+ """Get variables for the specified scope."""
416
+ match lookup_scope:
417
+ case VariableScope.CURRENT_FLOW_ONLY:
418
+ if starting_flow in self._flow_variables:
419
+ return list(self._flow_variables[starting_flow].values())
420
+ return []
421
+
422
+ case VariableScope.GLOBAL_ONLY:
423
+ return list(self._global_variables.values())
424
+
425
+ case VariableScope.HIERARCHICAL:
426
+ return self._get_hierarchical_variables(starting_flow)
427
+
428
+ case VariableScope.ALL:
429
+ return self._get_all_variables()
430
+
431
+ case _:
432
+ msg = f"Attempted to get variables from starting flow '{starting_flow}', but encountered an unknown/unexpected variable scope '{lookup_scope.value}'"
433
+ raise ValueError(msg)
434
+
435
+ def _get_hierarchical_variables(self, starting_flow: str) -> list[FlowVariable]:
436
+ """Get variables using hierarchical lookup with shadowing.
437
+
438
+ Variable shadowing behavior:
439
+ - Child flow variables shadow (hide) parent flow variables with same name
440
+ - Flow variables shadow global variables with same name
441
+
442
+ Example:
443
+ - Global: user_id = "global_user"
444
+ - Parent flow: user_id = "parent_user"
445
+ - Child flow: user_id = "child_user"
446
+ Result from child flow: only user_id = "child_user" (others are shadowed)
447
+ """
448
+ hierarchy = self._get_flow_hierarchy(starting_flow)
449
+ seen_names = set()
450
+ variables = []
451
+
452
+ # Add variables from flows (child to parent to implement shadowing)
453
+ for flow_name in hierarchy:
454
+ flow_vars = self._flow_variables.get(flow_name, {})
455
+ for var in flow_vars.values():
456
+ if var.name not in seen_names:
457
+ variables.append(var)
458
+ seen_names.add(var.name)
459
+
460
+ # Add global variables (lowest priority, can be shadowed by flow variables)
461
+ variables.extend(var for var in self._global_variables.values() if var.name not in seen_names)
462
+
463
+ return variables
464
+
465
+ def _get_all_variables(self) -> list[FlowVariable]:
466
+ """Get all variables from all flows for GUI enumeration.
467
+
468
+ Note: This returns ALL variables without shadowing - variables with the same name
469
+ from different flows/scopes will all be included in the result.
470
+ """
471
+ variables = []
472
+
473
+ # Add all flow variables (no shadowing - include all)
474
+ for flow_vars in self._flow_variables.values():
475
+ variables.extend(flow_vars.values())
476
+
477
+ # Add all global variables
478
+ variables.extend(self._global_variables.values())
479
+
480
+ return variables
481
+
482
+ def on_list_variables_request(self, request: ListVariablesRequest) -> ResultPayload:
483
+ """List all variables in the specified scope."""
484
+ try:
485
+ starting_flow = self._get_starting_flow(request.starting_flow)
486
+ except ValueError as e:
487
+ return ListVariablesResultFailure(
488
+ result_details=f"Attempted to list variables. Failed to determine starting flow: {e}"
489
+ )
490
+
491
+ variables = self._get_variables_by_scope(starting_flow, request.lookup_scope)
492
+
493
+ # Sort by name for consistent output
494
+ variables = sorted(variables, key=lambda v: v.name)
495
+ return ListVariablesResultSuccess(
496
+ variables=variables, result_details=f"Successfully listed {len(variables)} variables."
497
+ )
498
+
499
+ def on_get_variable_details_request(self, request: GetVariableDetailsRequest) -> ResultPayload:
500
+ """Get variable details (metadata only, no heavy values)."""
501
+ try:
502
+ starting_flow = self._get_starting_flow(request.starting_flow)
503
+ except ValueError as e:
504
+ return GetVariableDetailsResultFailure(
505
+ result_details=f"Attempted to get details for variable '{request.name}'. Failed to determine starting flow: {e}"
506
+ )
507
+
508
+ result = self._find_variable_hierarchical(starting_flow, request.name, request.lookup_scope)
509
+
510
+ if not result.variable:
511
+ return GetVariableDetailsResultFailure(
512
+ result_details=f"Attempted to get details for variable '{request.name}'. Failed because no such variable could be found."
513
+ )
514
+
515
+ variable = result.variable
516
+ details = VariableDetails(name=variable.name, owning_flow_name=variable.owning_flow_name, type=variable.type)
517
+ return GetVariableDetailsResultSuccess(
518
+ details=details, result_details=f"Successfully retrieved details for variable '{request.name}'."
519
+ )
520
+
521
+ def _find_variable_by_name(self, name: str) -> FlowVariable | None:
522
+ """Find a variable by name in current flow context (legacy compatibility)."""
523
+ try:
524
+ starting_flow = self._get_starting_flow(None)
525
+ except ValueError:
526
+ return None
527
+
528
+ result = self._find_variable_hierarchical(starting_flow, name, VariableScope.HIERARCHICAL)
529
+ return result.variable