griptape-nodes 0.46.0__py3-none-any.whl → 0.48.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 (25) hide show
  1. griptape_nodes/app/app.py +1 -1
  2. griptape_nodes/exe_types/core_types.py +129 -10
  3. griptape_nodes/exe_types/node_types.py +9 -3
  4. griptape_nodes/machines/node_resolution.py +10 -8
  5. griptape_nodes/mcp_server/ws_request_manager.py +6 -6
  6. griptape_nodes/retained_mode/events/base_events.py +74 -1
  7. griptape_nodes/retained_mode/events/secrets_events.py +2 -0
  8. griptape_nodes/retained_mode/griptape_nodes.py +17 -13
  9. griptape_nodes/retained_mode/managers/agent_manager.py +8 -6
  10. griptape_nodes/retained_mode/managers/arbitrary_code_exec_manager.py +1 -1
  11. griptape_nodes/retained_mode/managers/config_manager.py +36 -45
  12. griptape_nodes/retained_mode/managers/flow_manager.py +98 -98
  13. griptape_nodes/retained_mode/managers/library_manager.py +57 -57
  14. griptape_nodes/retained_mode/managers/node_manager.py +121 -124
  15. griptape_nodes/retained_mode/managers/object_manager.py +9 -10
  16. griptape_nodes/retained_mode/managers/os_manager.py +31 -31
  17. griptape_nodes/retained_mode/managers/secrets_manager.py +5 -5
  18. griptape_nodes/retained_mode/managers/static_files_manager.py +19 -21
  19. griptape_nodes/retained_mode/managers/sync_manager.py +3 -2
  20. griptape_nodes/retained_mode/managers/workflow_manager.py +153 -174
  21. griptape_nodes/retained_mode/retained_mode.py +25 -47
  22. {griptape_nodes-0.46.0.dist-info → griptape_nodes-0.48.0.dist-info}/METADATA +1 -1
  23. {griptape_nodes-0.46.0.dist-info → griptape_nodes-0.48.0.dist-info}/RECORD +25 -25
  24. {griptape_nodes-0.46.0.dist-info → griptape_nodes-0.48.0.dist-info}/WHEEL +1 -1
  25. {griptape_nodes-0.46.0.dist-info → griptape_nodes-0.48.0.dist-info}/entry_points.txt +0 -0
griptape_nodes/app/app.py CHANGED
@@ -118,7 +118,7 @@ def _ensure_api_key() -> str:
118
118
  "[code]gtn init --api-key <your key>[/code]\n"
119
119
  "[bold red]You can generate a new key from [/bold red][bold blue][link=https://nodes.griptape.ai]https://nodes.griptape.ai[/link][/bold blue]",
120
120
  ),
121
- title="🔑 Missing Nodes API Key",
121
+ title="[red]X[/red] Missing Nodes API Key",
122
122
  border_style="red",
123
123
  padding=(1, 4),
124
124
  )
@@ -65,25 +65,52 @@ class ParameterType:
65
65
  return ret_val
66
66
 
67
67
  @staticmethod
68
- def are_types_compatible(source_type: str | None, target_type: str | None) -> bool:
68
+ def _extract_base_type(type_str: str) -> str:
69
+ """Extract the base type from a potentially generic type string.
70
+
71
+ Examples:
72
+ 'list[any]' -> 'list'
73
+ 'dict[str, int]' -> 'dict'
74
+ 'str' -> 'str'
75
+ """
76
+ bracket_index = type_str.find("[")
77
+ if bracket_index == -1:
78
+ return type_str
79
+ return type_str[:bracket_index]
80
+
81
+ @staticmethod
82
+ def are_types_compatible(source_type: str | None, target_type: str | None) -> bool: # noqa: PLR0911
69
83
  if source_type is None or target_type is None:
70
84
  return False
71
85
 
72
- ret_val = False
73
86
  source_type_lower = source_type.lower()
74
87
  target_type_lower = target_type.lower()
75
88
 
76
89
  # If either are None, bail.
77
90
  if ParameterTypeBuiltin.NONE.value in (source_type_lower, target_type_lower):
78
- ret_val = False
79
- elif target_type_lower == ParameterTypeBuiltin.ANY.value:
91
+ return False
92
+ if target_type_lower == ParameterTypeBuiltin.ANY.value:
80
93
  # If the TARGET accepts Any, we're good. Not always true the other way 'round.
81
- ret_val = True
82
- else:
83
- # Do a compare.
84
- ret_val = source_type_lower == target_type_lower
94
+ return True
85
95
 
86
- return ret_val
96
+ # First try exact match
97
+ if source_type_lower == target_type_lower:
98
+ return True
99
+
100
+ source_base = ParameterType._extract_base_type(source_type_lower)
101
+ target_base = ParameterType._extract_base_type(target_type_lower)
102
+
103
+ # If base types match
104
+ if source_base == target_base:
105
+ # Allow any generic to flow to base type (list[any] -> list, list[str] -> list)
106
+ if target_type_lower == target_base:
107
+ return True
108
+
109
+ # Allow specific types to flow to [any] generic (list[str] -> list[any])
110
+ if target_type_lower == f"{target_base}[{ParameterTypeBuiltin.ANY.value}]":
111
+ return True
112
+
113
+ return False
87
114
 
88
115
  @staticmethod
89
116
  def parse_kv_type_pair(type_str: str) -> KeyValueTypePair | None: # noqa: C901
@@ -250,7 +277,6 @@ class BaseNodeElement:
250
277
  self._changes["ui_options"] = complete_dict["ui_options"]
251
278
 
252
279
  event_data.update(self._changes)
253
-
254
280
  # Publish the event
255
281
  event = ExecutionGriptapeNodeEvent(
256
282
  wrapped_event=ExecutionEvent(payload=AlterElementEvent(element_details=event_data))
@@ -1326,6 +1352,27 @@ class ParameterList(ParameterContainer):
1326
1352
  result = f"list[{base_type}]"
1327
1353
  return result
1328
1354
 
1355
+ def _custom_setter_for_property_type(self, value: str | None) -> None:
1356
+ # If we are setting a type, we need to propagate this to our children as well.
1357
+ for child in self._children:
1358
+ if isinstance(child, Parameter):
1359
+ child.type = value
1360
+ super()._custom_setter_for_property_type(value)
1361
+
1362
+ def _custom_setter_for_property_input_types(self, value: list[str] | None) -> None:
1363
+ # If we are setting a type, we need to propagate this to our children as well.
1364
+ for child in self._children:
1365
+ if isinstance(child, Parameter):
1366
+ child.input_types = value
1367
+ return super()._custom_setter_for_property_input_types(value)
1368
+
1369
+ def _custom_setter_for_property_output_type(self, value: str | None) -> None:
1370
+ # If we are setting a type, we need to propagate this to our children as well.
1371
+ for child in self._children:
1372
+ if isinstance(child, Parameter):
1373
+ child.output_type = value
1374
+ return super()._custom_setter_for_property_output_type(value)
1375
+
1329
1376
  def _custom_getter_for_property_input_types(self) -> list[str]:
1330
1377
  # For every valid input type, also accept a list variant of that for the CONTAINER Parameter only.
1331
1378
  # Children still use the input types given to them.
@@ -1395,6 +1442,78 @@ class ParameterList(ParameterContainer):
1395
1442
 
1396
1443
  return param
1397
1444
 
1445
+ def clear_list(self) -> None:
1446
+ """Remove all children that have been added to the list."""
1447
+ children = self.find_elements_by_type(element_type=Parameter)
1448
+ for child in children:
1449
+ if isinstance(child, Parameter):
1450
+ self.remove_child(child)
1451
+ del child
1452
+
1453
+ # --- Convenience methods for stable list management ---
1454
+ def get_child_parameters(self) -> list[Parameter]:
1455
+ """Return direct child parameters only, in order of appearance."""
1456
+ return self.find_elements_by_type(element_type=Parameter, find_recursively=False)
1457
+
1458
+ def append_child_parameter(self, display_name: str | None = None) -> Parameter:
1459
+ """Append one child parameter and optionally set a display name.
1460
+
1461
+ This preserves existing children and adds a new one at the end.
1462
+ """
1463
+ child = self.add_child_parameter()
1464
+ if display_name is not None:
1465
+ ui_opts = child.ui_options or {}
1466
+ ui_opts["display_name"] = display_name
1467
+ child.ui_options = ui_opts
1468
+ return child
1469
+
1470
+ def remove_last_child_parameter(self) -> None:
1471
+ """Remove the last child parameter if one exists.
1472
+
1473
+ This removes from the end to preserve earlier children and their connections.
1474
+ """
1475
+ children = self.get_child_parameters()
1476
+ if children:
1477
+ last = children[-1]
1478
+ self.remove_child(last)
1479
+ del last
1480
+
1481
+ def ensure_length(self, desired_count: int, display_name_prefix: str | None = None) -> None:
1482
+ """Grow or shrink the list to the desired length while preserving existing items.
1483
+
1484
+ - If increasing, appends new children to the end.
1485
+ - If decreasing, removes children from the end.
1486
+ - Optionally sets display names like "{prefix} 1", "{prefix} 2", ...
1487
+ """
1488
+ if desired_count is None:
1489
+ return
1490
+ try:
1491
+ desired_count = int(desired_count)
1492
+ except Exception:
1493
+ desired_count = 0
1494
+ desired_count = max(desired_count, 0)
1495
+
1496
+ current_children = self.get_child_parameters()
1497
+ current_len = len(current_children)
1498
+
1499
+ # Grow
1500
+ if current_len < desired_count:
1501
+ for index in range(current_len, desired_count):
1502
+ name = f"{display_name_prefix} {index + 1}" if display_name_prefix else None
1503
+ self.append_child_parameter(display_name=name)
1504
+
1505
+ # Shrink
1506
+ elif current_len > desired_count:
1507
+ for _ in range(current_len - desired_count):
1508
+ self.remove_last_child_parameter()
1509
+
1510
+ # Optionally re-apply display names to existing children to keep indices tidy
1511
+ if display_name_prefix:
1512
+ for index, child in enumerate(self.get_child_parameters()):
1513
+ ui_opts = child.ui_options or {}
1514
+ ui_opts["display_name"] = f"{display_name_prefix} {index + 1}"
1515
+ child.ui_options = ui_opts
1516
+
1398
1517
  def add_child(self, child: BaseNodeElement) -> None:
1399
1518
  """Override to mark parent node as unresolved when children are added.
1400
1519
 
@@ -848,12 +848,12 @@ class BaseNode(ABC):
848
848
 
849
849
  # Create event data using the parameter's to_event method
850
850
  if remove:
851
+ # Import logger here to avoid circular dependency
851
852
  event = ExecutionGriptapeNodeEvent(
852
853
  wrapped_event=ExecutionEvent(payload=RemoveElementEvent(element_id=parameter.element_id))
853
854
  )
854
855
  else:
855
856
  event_data = parameter.to_event(self)
856
-
857
857
  # Publish the event
858
858
  event = ExecutionGriptapeNodeEvent(
859
859
  wrapped_event=ExecutionEvent(payload=AlterElementEvent(element_details=event_data))
@@ -1053,11 +1053,17 @@ class EndNode(BaseNode):
1053
1053
 
1054
1054
 
1055
1055
  class StartLoopNode(BaseNode):
1056
- finished: bool
1057
- current_index: int
1058
1056
  end_node: EndLoopNode | None = None
1059
1057
  """Creating class for Start Loop Node in order to implement loop functionality in execution."""
1060
1058
 
1059
+ @abstractmethod
1060
+ def is_loop_finished(self) -> bool:
1061
+ """Return True if the loop has finished executing.
1062
+
1063
+ This method must be implemented by subclasses to define when
1064
+ the loop should terminate.
1065
+ """
1066
+
1061
1067
 
1062
1068
  class EndLoopNode(BaseNode):
1063
1069
  start_node: StartLoopNode | None = None
@@ -225,16 +225,18 @@ class ExecuteNodeState(State):
225
225
  # If the upstream node is resolved, collect its output value
226
226
  if upstream_parameter.name in upstream_node.parameter_output_values:
227
227
  output_value = upstream_node.parameter_output_values[upstream_parameter.name]
228
+ else:
229
+ output_value = upstream_node.get_parameter_value(upstream_parameter.name)
228
230
 
229
- # Pass the value through using the same mechanism as normal resolution
230
- GriptapeNodes.get_instance().handle_request(
231
- SetParameterValueRequest(
232
- parameter_name=parameter.name,
233
- node_name=current_node.name,
234
- value=output_value,
235
- data_type=upstream_parameter.output_type,
236
- )
231
+ # Pass the value through using the same mechanism as normal resolution
232
+ GriptapeNodes.get_instance().handle_request(
233
+ SetParameterValueRequest(
234
+ parameter_name=parameter.name,
235
+ node_name=current_node.name,
236
+ value=output_value,
237
+ data_type=upstream_parameter.output_type,
237
238
  )
239
+ )
238
240
 
239
241
  @staticmethod
240
242
  def on_enter(context: ResolutionContext) -> type[State] | None:
@@ -41,7 +41,7 @@ class WebSocketConnectionManager:
41
41
  try:
42
42
  message = json.dumps(data)
43
43
  await self.websocket.send(message)
44
- logger.debug("📤 Sent message: %s", message)
44
+ logger.debug("Sent message: %s", message)
45
45
  except Exception as e:
46
46
  logger.error("Failed to send message: %s", e)
47
47
  raise
@@ -157,7 +157,7 @@ class AsyncRequestManager(Generic[T]): # noqa: UP046
157
157
 
158
158
  except Exception as e:
159
159
  self.connection_manager.connected = False
160
- logger.error("🔴 WebSocket connection failed: %s", str(e))
160
+ logger.error("[red]X[/red] WebSocket connection failed: %s", str(e))
161
161
  msg = f"Failed to connect to WebSocket: {e!s}"
162
162
  raise ConnectionError(msg) from e
163
163
 
@@ -192,7 +192,7 @@ class AsyncRequestManager(Generic[T]): # noqa: UP046
192
192
  """Send an event to the API without waiting for a response."""
193
193
  from griptape_nodes.app.app import _determine_request_topic
194
194
 
195
- logger.debug("📝 Creating Event: %s - %s", request_type, json.dumps(payload))
195
+ logger.debug("Creating Event: %s - %s", request_type, json.dumps(payload))
196
196
 
197
197
  data = {"event_type": "EventRequest", "request_type": request_type, "request": payload}
198
198
  topic = _determine_request_topic()
@@ -231,7 +231,7 @@ class AsyncRequestManager(Generic[T]): # noqa: UP046
231
231
  def success_handler(response: Any, _: Any) -> None:
232
232
  if not response_future.done():
233
233
  result = response.get("payload", {}).get("result", "Success")
234
- logger.debug(" Request succeeded: %s", result)
234
+ logger.debug("[green]OK[/green] Request succeeded: %s", result)
235
235
  response_future.set_result(result)
236
236
 
237
237
  def failure_handler(response: Any, _: Any) -> None:
@@ -239,14 +239,14 @@ class AsyncRequestManager(Generic[T]): # noqa: UP046
239
239
  error = (
240
240
  response.get("payload", {}).get("result", {}).get("exception", "Unknown error") or "Unknown error"
241
241
  )
242
- logger.error(" Request failed: %s", error)
242
+ logger.error("[red]X[/red] Request failed: %s", error)
243
243
  response_future.set_exception(Exception(error))
244
244
 
245
245
  # Generate request ID and subscribe
246
246
  request_id = self.connection_manager.subscribe_to_request_event(success_handler, failure_handler)
247
247
  payload["request_id"] = request_id
248
248
 
249
- logger.debug("🚀 Request (%s): %s %s", request_id, request_type, json.dumps(payload))
249
+ logger.debug("Request (%s): %s %s", request_id, request_type, json.dumps(payload))
250
250
 
251
251
  try:
252
252
  # Send the event
@@ -1,9 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import logging
4
5
  from abc import ABC, abstractmethod
5
6
  from dataclasses import asdict, dataclass, field, is_dataclass
6
- from typing import TYPE_CHECKING, Any, ClassVar, TypeVar
7
+ from typing import TYPE_CHECKING, Any, ClassVar, Literal, TypeVar
7
8
 
8
9
  from griptape.artifacts import BaseArtifact
9
10
  from griptape.events import BaseEvent as GtBaseEvent
@@ -16,6 +17,64 @@ if TYPE_CHECKING:
16
17
  import builtins
17
18
 
18
19
 
20
+ @dataclass
21
+ class ResultDetail:
22
+ """A single detail about an operation result, including logging level and human readable message."""
23
+
24
+ level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
25
+ message: str
26
+
27
+
28
+ @dataclass
29
+ class ResultDetails:
30
+ """Container for multiple ResultDetail objects."""
31
+
32
+ result_details: list[ResultDetail]
33
+
34
+ def __init__(
35
+ self,
36
+ *result_details: ResultDetail,
37
+ message: str | None = None,
38
+ level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None,
39
+ logger: logging.Logger | str | None = "griptape_nodes",
40
+ ):
41
+ """Initialize with ResultDetail objects or create a single one from message/level.
42
+
43
+ Args:
44
+ *result_details: Variable number of ResultDetail objects
45
+ message: If provided, creates a single ResultDetail with this message
46
+ level: Logging level for the single ResultDetail (required if message is provided)
47
+ logger: Logger to use for auto-logging. String for logger name, Logger object, or None to skip
48
+ """
49
+ # Handle single message/level convenience
50
+ if message is not None:
51
+ if level is None:
52
+ err_msg = "level is required when message is provided"
53
+ raise ValueError(err_msg)
54
+ if result_details:
55
+ err_msg = "Cannot provide both result_details and message/level"
56
+ raise ValueError(err_msg)
57
+ self.result_details = [ResultDetail(level=level, message=message)]
58
+ else:
59
+ if not result_details:
60
+ err_msg = "ResultDetails requires at least one ResultDetail or message/level"
61
+ raise ValueError(err_msg)
62
+ self.result_details = list(result_details)
63
+
64
+ # Auto-log if logger is provided
65
+ if logger is not None:
66
+ try:
67
+ if isinstance(logger, str):
68
+ logger = logging.getLogger(logger)
69
+
70
+ for detail in self.result_details:
71
+ numeric_level = getattr(logging, detail.level)
72
+ logger.log(numeric_level, detail.message)
73
+ except Exception: # noqa: S110
74
+ # If logging fails for any reason, don't let it break the ResultDetails creation
75
+ pass
76
+
77
+
19
78
  # The Payload class is a marker interface
20
79
  class Payload(ABC): # noqa: B024
21
80
  """Base class for all payload types. Customers will derive from this."""
@@ -32,6 +91,7 @@ class RequestPayload(Payload, ABC):
32
91
  class ResultPayload(Payload, ABC):
33
92
  """Base class for all result payloads."""
34
93
 
94
+ result_details: ResultDetails | str
35
95
  """When set to True, alerts clients that this result made changes to the workflow state.
36
96
  Editors can use this to determine if the workflow is dirty and needs to be re-saved, for example."""
37
97
  altered_workflow_state: bool = False
@@ -76,6 +136,13 @@ class SkipTheLineMixin:
76
136
  class ResultPayloadSuccess(ResultPayload, ABC):
77
137
  """Abstract base class for success result payloads."""
78
138
 
139
+ result_details: ResultDetails | str = "Success"
140
+
141
+ def __post_init__(self) -> None:
142
+ """Initialize success result with INFO level default for strings."""
143
+ if isinstance(self.result_details, str):
144
+ self.result_details = ResultDetails(message=self.result_details, level="DEBUG")
145
+
79
146
  def succeeded(self) -> bool:
80
147
  """Returns True as this is a success result.
81
148
 
@@ -90,8 +157,14 @@ class ResultPayloadSuccess(ResultPayload, ABC):
90
157
  class ResultPayloadFailure(ResultPayload, ABC):
91
158
  """Abstract base class for failure result payloads."""
92
159
 
160
+ result_details: ResultDetails | str = "Failure"
93
161
  exception: Exception | None = None
94
162
 
163
+ def __post_init__(self) -> None:
164
+ """Initialize failure result with ERROR level default for strings."""
165
+ if isinstance(self.result_details, str):
166
+ self.result_details = ResultDetails(message=self.result_details, level="ERROR")
167
+
95
168
  def succeeded(self) -> bool:
96
169
  """Returns False as this is a failure result.
97
170
 
@@ -20,11 +20,13 @@ class GetSecretValueRequest(RequestPayload):
20
20
 
21
21
  Args:
22
22
  key: Name of the secret key to retrieve
23
+ should_error_on_not_found: Whether to error if the key is not found (default: True)
23
24
 
24
25
  Results: GetSecretValueResultSuccess (with value) | GetSecretValueResultFailure (key not found)
25
26
  """
26
27
 
27
28
  key: str
29
+ should_error_on_not_found: bool = True
28
30
 
29
31
 
30
32
  @dataclass
@@ -389,11 +389,11 @@ class GriptapeNodes(metaclass=SingletonMeta):
389
389
  )
390
390
  details = f"Attempted to get engine version. Failed because version string '{engine_ver}' wasn't in expected major.minor.patch format."
391
391
  logger.error(details)
392
- return GetEngineVersionResultFailure()
392
+ return GetEngineVersionResultFailure(result_details=details)
393
393
  except Exception as err:
394
394
  details = f"Attempted to get engine version. Failed due to '{err}'."
395
395
  logger.error(details)
396
- return GetEngineVersionResultFailure()
396
+ return GetEngineVersionResultFailure(result_details=details)
397
397
 
398
398
  def handle_session_start_request(self, request: AppStartSessionRequest) -> ResultPayload: # noqa: ARG002
399
399
  from griptape_nodes.app.app import subscribe_to_topic
@@ -434,7 +434,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
434
434
  except Exception as err:
435
435
  details = f"Failed to end session due to '{err}'."
436
436
  logger.error(details)
437
- return AppEndSessionResultFailure()
437
+ return AppEndSessionResultFailure(result_details=details)
438
438
 
439
439
  def handle_get_session_request(self, _: AppGetSessionRequest) -> ResultPayload:
440
440
  return AppGetSessionResultSuccess(session_id=GriptapeNodes.SessionManager().get_active_session_id())
@@ -447,14 +447,17 @@ class GriptapeNodes(metaclass=SingletonMeta):
447
447
  try:
448
448
  active_session_id = GriptapeNodes.SessionManager().get_active_session_id()
449
449
  if active_session_id is None:
450
- logger.warning("Session heartbeat received but no active session found")
451
- return SessionHeartbeatResultFailure()
450
+ details = "Session heartbeat received but no active session found"
451
+ logger.warning(details)
452
+ return SessionHeartbeatResultFailure(result_details=details)
452
453
 
453
- logger.debug("Session heartbeat successful for session: %s", active_session_id)
454
+ details = f"Session heartbeat successful for session: {active_session_id}"
455
+ logger.debug(details)
454
456
  return SessionHeartbeatResultSuccess()
455
457
  except Exception as err:
456
- logger.error("Failed to handle session heartbeat: %s", err)
457
- return SessionHeartbeatResultFailure()
458
+ details = f"Failed to handle session heartbeat: {err}"
459
+ logger.error(details)
460
+ return SessionHeartbeatResultFailure(result_details=details)
458
461
 
459
462
  def handle_engine_heartbeat_request(self, request: EngineHeartbeatRequest) -> ResultPayload:
460
463
  """Handle engine heartbeat requests.
@@ -483,8 +486,9 @@ class GriptapeNodes(metaclass=SingletonMeta):
483
486
  **workflow_info,
484
487
  )
485
488
  except Exception as err:
486
- logger.error("Failed to handle engine heartbeat: %s", err)
487
- return EngineHeartbeatResultFailure(heartbeat_id=request.heartbeat_id)
489
+ details = f"Failed to handle engine heartbeat: {err}"
490
+ logger.error(details)
491
+ return EngineHeartbeatResultFailure(heartbeat_id=request.heartbeat_id, result_details=details)
488
492
 
489
493
  def handle_get_engine_name_request(self, request: GetEngineNameRequest) -> ResultPayload: # noqa: ARG002
490
494
  """Handle requests to get the current engine name."""
@@ -495,7 +499,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
495
499
  except Exception as err:
496
500
  error_message = f"Failed to get engine name: {err}"
497
501
  logger.error(error_message)
498
- return GetEngineNameResultFailure(error_message=error_message)
502
+ return GetEngineNameResultFailure(error_message=error_message, result_details=error_message)
499
503
 
500
504
  def handle_set_engine_name_request(self, request: SetEngineNameRequest) -> ResultPayload:
501
505
  """Handle requests to set a new engine name."""
@@ -504,7 +508,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
504
508
  if not request.engine_name or not request.engine_name.strip():
505
509
  error_message = "Engine name cannot be empty"
506
510
  logger.warning(error_message)
507
- return SetEngineNameResultFailure(error_message=error_message)
511
+ return SetEngineNameResultFailure(error_message=error_message, result_details=error_message)
508
512
 
509
513
  # Set the new engine name
510
514
  GriptapeNodes.EngineIdentityManager().set_engine_name(request.engine_name.strip())
@@ -514,7 +518,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
514
518
  except Exception as err:
515
519
  error_message = f"Failed to set engine name: {err}"
516
520
  logger.error(error_message)
517
- return SetEngineNameResultFailure(error_message=error_message)
521
+ return SetEngineNameResultFailure(error_message=error_message, result_details=error_message)
518
522
 
519
523
  def _get_instance_info(self) -> dict[str, str | None]:
520
524
  """Get instance information from environment variables.
@@ -204,16 +204,18 @@ class AgentManager:
204
204
  pass # Ignore incomplete JSON
205
205
  if isinstance(last_event, FinishTaskEvent):
206
206
  if isinstance(last_event.task_output, ErrorArtifact):
207
- return RunAgentResultFailure(last_event.task_output.to_dict())
207
+ return RunAgentResultFailure(
208
+ error=last_event.task_output.to_dict(), result_details=last_event.task_output.to_json()
209
+ )
208
210
  if isinstance(last_event.task_output, JsonArtifact):
209
211
  return RunAgentResultSuccess(last_event.task_output.to_dict())
210
212
  err_msg = f"Unexpected final event: {last_event}"
211
213
  logger.error(err_msg)
212
- return RunAgentResultFailure(ErrorArtifact(last_event).to_dict())
214
+ return RunAgentResultFailure(error=ErrorArtifact(last_event).to_dict(), result_details=err_msg)
213
215
  except Exception as e:
214
216
  err_msg = f"Error running agent: {e}"
215
217
  logger.error(err_msg)
216
- return RunAgentResultFailure(ErrorArtifact(e).to_dict())
218
+ return RunAgentResultFailure(error=ErrorArtifact(e).to_dict(), result_details=err_msg)
217
219
 
218
220
  def on_handle_configure_agent_request(self, request: ConfigureAgentRequest) -> ResultPayload:
219
221
  try:
@@ -224,7 +226,7 @@ class AgentManager:
224
226
  except Exception as e:
225
227
  details = f"Error configuring agent: {e}"
226
228
  logger.error(details)
227
- return ConfigureAgentResultFailure()
229
+ return ConfigureAgentResultFailure(result_details=details)
228
230
  return ConfigureAgentResultSuccess()
229
231
 
230
232
  def on_handle_reset_agent_conversation_memory_request(
@@ -235,7 +237,7 @@ class AgentManager:
235
237
  except Exception as e:
236
238
  details = f"Error resetting agent conversation memory: {e}"
237
239
  logger.error(details)
238
- return ResetAgentConversationMemoryResultFailure()
240
+ return ResetAgentConversationMemoryResultFailure(result_details=details)
239
241
  return ResetAgentConversationMemoryResultSuccess()
240
242
 
241
243
  def on_handle_get_conversation_memory_request(self, _: GetConversationMemoryRequest) -> ResultPayload:
@@ -244,5 +246,5 @@ class AgentManager:
244
246
  except Exception as e:
245
247
  details = f"Error getting conversation memory: {e}"
246
248
  logger.error(details)
247
- return GetConversationMemoryResultFailure()
249
+ return GetConversationMemoryResultFailure(result_details=details)
248
250
  return GetConversationMemoryResultSuccess(runs=conversation_memory)
@@ -46,6 +46,6 @@ class ArbitraryCodeExecManager:
46
46
  result = RunArbitraryPythonStringResultSuccess(python_output=captured_output)
47
47
  except Exception as e:
48
48
  python_output = f"ERROR: {e}"
49
- result = RunArbitraryPythonStringResultFailure(python_output=python_output)
49
+ result = RunArbitraryPythonStringResultFailure(python_output=python_output, result_details=python_output)
50
50
 
51
51
  return result