griptape-nodes 0.60.4__py3-none-any.whl → 0.61.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 (47) hide show
  1. griptape_nodes/bootstrap/workflow_publishers/local_workflow_publisher.py +0 -1
  2. griptape_nodes/common/macro_parser/__init__.py +16 -1
  3. griptape_nodes/common/macro_parser/core.py +15 -3
  4. griptape_nodes/common/macro_parser/exceptions.py +99 -0
  5. griptape_nodes/common/macro_parser/formats.py +13 -4
  6. griptape_nodes/common/macro_parser/matching.py +5 -2
  7. griptape_nodes/common/macro_parser/parsing.py +48 -8
  8. griptape_nodes/common/macro_parser/resolution.py +23 -5
  9. griptape_nodes/common/project_templates/__init__.py +49 -0
  10. griptape_nodes/common/project_templates/default_project_template.py +92 -0
  11. griptape_nodes/common/project_templates/defaults/README.md +36 -0
  12. griptape_nodes/common/project_templates/defaults/project_template.yml +89 -0
  13. griptape_nodes/common/project_templates/directory.py +67 -0
  14. griptape_nodes/common/project_templates/loader.py +341 -0
  15. griptape_nodes/common/project_templates/project.py +252 -0
  16. griptape_nodes/common/project_templates/situation.py +155 -0
  17. griptape_nodes/common/project_templates/validation.py +140 -0
  18. griptape_nodes/exe_types/core_types.py +36 -3
  19. griptape_nodes/exe_types/node_types.py +4 -2
  20. griptape_nodes/exe_types/param_components/progress_bar_component.py +57 -0
  21. griptape_nodes/exe_types/param_types/parameter_audio.py +243 -0
  22. griptape_nodes/exe_types/param_types/parameter_image.py +243 -0
  23. griptape_nodes/exe_types/param_types/parameter_three_d.py +215 -0
  24. griptape_nodes/exe_types/param_types/parameter_video.py +243 -0
  25. griptape_nodes/node_library/workflow_registry.py +1 -1
  26. griptape_nodes/retained_mode/events/execution_events.py +41 -0
  27. griptape_nodes/retained_mode/events/node_events.py +90 -1
  28. griptape_nodes/retained_mode/events/os_events.py +108 -0
  29. griptape_nodes/retained_mode/events/parameter_events.py +1 -1
  30. griptape_nodes/retained_mode/events/project_events.py +413 -0
  31. griptape_nodes/retained_mode/events/workflow_events.py +19 -1
  32. griptape_nodes/retained_mode/griptape_nodes.py +9 -1
  33. griptape_nodes/retained_mode/managers/agent_manager.py +18 -24
  34. griptape_nodes/retained_mode/managers/event_manager.py +6 -9
  35. griptape_nodes/retained_mode/managers/flow_manager.py +63 -0
  36. griptape_nodes/retained_mode/managers/library_manager.py +55 -42
  37. griptape_nodes/retained_mode/managers/mcp_manager.py +14 -6
  38. griptape_nodes/retained_mode/managers/node_manager.py +232 -0
  39. griptape_nodes/retained_mode/managers/os_manager.py +345 -0
  40. griptape_nodes/retained_mode/managers/project_manager.py +617 -0
  41. griptape_nodes/retained_mode/managers/settings.py +6 -0
  42. griptape_nodes/retained_mode/managers/workflow_manager.py +6 -69
  43. griptape_nodes/traits/button.py +18 -0
  44. {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.61.0.dist-info}/METADATA +5 -3
  45. {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.61.0.dist-info}/RECORD +47 -31
  46. {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.61.0.dist-info}/WHEEL +1 -1
  47. {griptape_nodes-0.60.4.dist-info → griptape_nodes-0.61.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,413 @@
1
+ """Events for project template management."""
2
+
3
+ from dataclasses import dataclass
4
+ from enum import StrEnum
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from griptape_nodes.common.macro_parser import MacroMatchFailure, MacroParseFailure, VariableInfo
9
+ from griptape_nodes.common.project_templates import ProjectTemplate, ProjectValidationInfo
10
+ from griptape_nodes.retained_mode.events.base_events import (
11
+ RequestPayload,
12
+ ResultPayloadFailure,
13
+ ResultPayloadSuccess,
14
+ WorkflowNotAlteredMixin,
15
+ )
16
+ from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
17
+
18
+ # Type alias for macro variable dictionaries (used by ParsedMacro)
19
+ MacroVariables = dict[str, str | int]
20
+
21
+
22
+ class PathResolutionFailureReason(StrEnum):
23
+ """Reason why path resolution from macro failed."""
24
+
25
+ MISSING_REQUIRED_VARIABLES = "MISSING_REQUIRED_VARIABLES"
26
+ MACRO_RESOLUTION_ERROR = "MACRO_RESOLUTION_ERROR"
27
+ DIRECTORY_OVERRIDE_ATTEMPTED = "DIRECTORY_OVERRIDE_ATTEMPTED"
28
+
29
+
30
+ @dataclass
31
+ @PayloadRegistry.register
32
+ class LoadProjectTemplateRequest(RequestPayload):
33
+ """Load user's project.yml and merge with system defaults.
34
+
35
+ Use when: User opens a workspace, user creates new project, user modifies project.yml.
36
+
37
+ Args:
38
+ project_path: Path to the project.yml file to load
39
+
40
+ Results: LoadProjectTemplateResultSuccess | LoadProjectTemplateResultFailure
41
+ """
42
+
43
+ project_path: Path
44
+
45
+
46
+ @dataclass
47
+ @PayloadRegistry.register
48
+ class LoadProjectTemplateResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
49
+ """Project template loaded successfully.
50
+
51
+ Args:
52
+ project_path: Path to the loaded project.yml
53
+ template: The merged ProjectTemplate (system defaults + user customizations)
54
+ validation: Validation info with status and any problems encountered
55
+ """
56
+
57
+ project_path: Path
58
+ template: ProjectTemplate
59
+ validation: ProjectValidationInfo
60
+
61
+
62
+ @dataclass
63
+ @PayloadRegistry.register
64
+ class LoadProjectTemplateResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
65
+ """Project template loading failed.
66
+
67
+ Args:
68
+ project_path: Path to the project.yml that failed to load
69
+ validation: Validation info with error details
70
+ """
71
+
72
+ project_path: Path
73
+ validation: ProjectValidationInfo
74
+
75
+
76
+ @dataclass
77
+ @PayloadRegistry.register
78
+ class GetProjectTemplateRequest(RequestPayload):
79
+ """Get cached project template for a workspace path.
80
+
81
+ Use when: Querying current project configuration, checking validation status.
82
+
83
+ Args:
84
+ project_path: Path to the project.yml file
85
+
86
+ Results: GetProjectTemplateResultSuccess | GetProjectTemplateResultFailure
87
+ """
88
+
89
+ project_path: Path
90
+
91
+
92
+ @dataclass
93
+ @PayloadRegistry.register
94
+ class GetProjectTemplateResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
95
+ """Project template retrieved from cache.
96
+
97
+ Args:
98
+ template: The successfully loaded ProjectTemplate
99
+ validation: Validation info for the template
100
+ """
101
+
102
+ template: ProjectTemplate
103
+ validation: ProjectValidationInfo
104
+
105
+
106
+ @dataclass
107
+ @PayloadRegistry.register
108
+ class GetProjectTemplateResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
109
+ """Project template retrieval failed (not loaded yet)."""
110
+
111
+
112
+ @dataclass
113
+ @PayloadRegistry.register
114
+ class GetMacroForSituationRequest(RequestPayload):
115
+ """Get the macro schema for a specific situation.
116
+
117
+ Use when: Need to know what variables a situation requires, or get schema for custom resolution.
118
+
119
+ Args:
120
+ project_path: Path to the project.yml to use
121
+ situation_name: Name of the situation template (e.g., "save_node_output")
122
+
123
+ Results: GetMacroForSituationResultSuccess | GetMacroForSituationResultFailure
124
+ """
125
+
126
+ project_path: Path
127
+ situation_name: str
128
+
129
+
130
+ @dataclass
131
+ @PayloadRegistry.register
132
+ class GetMacroForSituationResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
133
+ """Situation macro retrieved successfully.
134
+
135
+ Args:
136
+ macro_schema: The macro template string (e.g., "{inputs}/{file_name}.{file_ext}")
137
+ """
138
+
139
+ macro_schema: str
140
+
141
+
142
+ @dataclass
143
+ @PayloadRegistry.register
144
+ class GetMacroForSituationResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
145
+ """Situation macro retrieval failed (situation not found or template not loaded)."""
146
+
147
+
148
+ @dataclass
149
+ @PayloadRegistry.register
150
+ class GetPathForMacroRequest(RequestPayload):
151
+ """Resolve ANY macro schema with variables to produce final file path.
152
+
153
+ Use when: Resolving paths, saving files. Works with any macro string, not tied to situations.
154
+
155
+ Args:
156
+ project_path: Path to the project.yml (used for directory resolution)
157
+ macro_schema: The macro template string to resolve (e.g., "{inputs}/{file_name}.{file_ext}")
158
+ variables: Variable values for macro substitution (e.g., {"file_name": "output", "file_ext": "png"})
159
+
160
+ Results: GetPathForMacroResultSuccess | GetPathForMacroResultFailure
161
+ """
162
+
163
+ project_path: Path
164
+ macro_schema: str
165
+ variables: MacroVariables
166
+
167
+
168
+ @dataclass
169
+ @PayloadRegistry.register
170
+ class GetPathForMacroResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
171
+ """Path resolved successfully from macro.
172
+
173
+ Args:
174
+ resolved_path: The final Path after macro substitution
175
+ """
176
+
177
+ resolved_path: Path
178
+
179
+
180
+ @dataclass
181
+ @PayloadRegistry.register
182
+ class GetPathForMacroResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
183
+ """Path resolution failed.
184
+
185
+ Args:
186
+ failure_reason: Specific reason for failure
187
+ missing_variables: List of required variable names that were not provided (for MISSING_REQUIRED_VARIABLES)
188
+ conflicting_variables: List of variables that conflict with directory names (for DIRECTORY_OVERRIDE_ATTEMPTED)
189
+ error_details: Additional error details from ParsedMacro (for MACRO_RESOLUTION_ERROR)
190
+ """
191
+
192
+ failure_reason: PathResolutionFailureReason
193
+ missing_variables: list[str] | None = None
194
+ conflicting_variables: list[str] | None = None
195
+ error_details: str | None = None
196
+
197
+
198
+ @dataclass
199
+ @PayloadRegistry.register
200
+ class SetCurrentProjectRequest(RequestPayload):
201
+ """Set which project.yml user has currently selected.
202
+
203
+ Use when: User switches between projects, opens a new workspace.
204
+
205
+ Args:
206
+ project_path: Path to the project.yml to set as current (None to clear)
207
+
208
+ Results: SetCurrentProjectResultSuccess
209
+ """
210
+
211
+ project_path: Path | None
212
+
213
+
214
+ @dataclass
215
+ @PayloadRegistry.register
216
+ class SetCurrentProjectResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
217
+ """Current project set successfully."""
218
+
219
+
220
+ @dataclass
221
+ @PayloadRegistry.register
222
+ class GetCurrentProjectRequest(RequestPayload):
223
+ """Get the currently selected project path.
224
+
225
+ Use when: Need to know which project user is working with.
226
+
227
+ Results: GetCurrentProjectResultSuccess
228
+ """
229
+
230
+
231
+ @dataclass
232
+ @PayloadRegistry.register
233
+ class GetCurrentProjectResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
234
+ """Current project retrieved.
235
+
236
+ Args:
237
+ project_path: The currently selected project path ("No Project" if None)
238
+ """
239
+
240
+ project_path: Path | None
241
+
242
+
243
+ @dataclass
244
+ @PayloadRegistry.register
245
+ class SaveProjectTemplateRequest(RequestPayload):
246
+ """Save user customizations to project.yml file.
247
+
248
+ Use when: User modifies project configuration, exports template.
249
+
250
+ Args:
251
+ project_path: Path where project.yml should be saved
252
+ template_data: Dict representation of the template to save
253
+
254
+ Results: SaveProjectTemplateResultSuccess | SaveProjectTemplateResultFailure
255
+ """
256
+
257
+ project_path: Path
258
+ template_data: dict[str, Any]
259
+
260
+
261
+ @dataclass
262
+ @PayloadRegistry.register
263
+ class SaveProjectTemplateResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
264
+ """Project template saved successfully.
265
+
266
+ Args:
267
+ project_path: Path where project.yml was saved
268
+ """
269
+
270
+ project_path: Path
271
+
272
+
273
+ @dataclass
274
+ @PayloadRegistry.register
275
+ class SaveProjectTemplateResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
276
+ """Project template save failed.
277
+
278
+ Common causes:
279
+ - Permission denied
280
+ - Invalid path
281
+ - Disk full
282
+ """
283
+
284
+ project_path: Path
285
+
286
+
287
+ @dataclass
288
+ @PayloadRegistry.register
289
+ class MatchPathAgainstMacroRequest(RequestPayload):
290
+ """Check if a path matches a macro schema and extract variables.
291
+
292
+ Use when: Validating paths, extracting info from file paths,
293
+ identifying which schema produced a file.
294
+
295
+ Args:
296
+ project_path: Path to project.yml (for directory resolution)
297
+ macro_schema: Macro template string
298
+ file_path: Path to test
299
+ known_variables: Variables we already know
300
+
301
+ Results: MatchPathAgainstMacroResultSuccess | MatchPathAgainstMacroResultFailure
302
+ """
303
+
304
+ project_path: Path
305
+ macro_schema: str
306
+ file_path: str
307
+ known_variables: MacroVariables
308
+
309
+
310
+ @dataclass
311
+ @PayloadRegistry.register
312
+ class MatchPathAgainstMacroResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
313
+ """Path matched the macro schema."""
314
+
315
+ extracted_variables: MacroVariables
316
+
317
+
318
+ @dataclass
319
+ @PayloadRegistry.register
320
+ class MatchPathAgainstMacroResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
321
+ """Path did not match the macro schema."""
322
+
323
+ match_failure: MacroMatchFailure
324
+
325
+
326
+ @dataclass
327
+ @PayloadRegistry.register
328
+ class GetVariablesForMacroRequest(RequestPayload):
329
+ """Get list of all variables in a macro schema.
330
+
331
+ Use when: Building UI forms, showing what variables a schema needs,
332
+ validating before resolution.
333
+
334
+ Args:
335
+ macro_schema: Macro template string to inspect
336
+
337
+ Results: GetVariablesForMacroResultSuccess | GetVariablesForMacroResultFailure
338
+ """
339
+
340
+ macro_schema: str
341
+
342
+
343
+ @dataclass
344
+ @PayloadRegistry.register
345
+ class GetVariablesForMacroResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
346
+ """Variables found in the macro schema."""
347
+
348
+ variables: list[VariableInfo]
349
+
350
+
351
+ @dataclass
352
+ @PayloadRegistry.register
353
+ class GetVariablesForMacroResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
354
+ """Failed to parse macro schema."""
355
+
356
+ parse_failure: MacroParseFailure
357
+
358
+
359
+ @dataclass
360
+ @PayloadRegistry.register
361
+ class ValidateMacroSyntaxRequest(RequestPayload):
362
+ """Validate a macro schema string for syntax errors.
363
+
364
+ Use when: User editing schemas in UI, before saving templates,
365
+ real-time validation.
366
+
367
+ Args:
368
+ macro_schema: Schema string to validate
369
+
370
+ Results: ValidateMacroSyntaxResultSuccess | ValidateMacroSyntaxResultFailure
371
+ """
372
+
373
+ macro_schema: str
374
+
375
+
376
+ @dataclass
377
+ @PayloadRegistry.register
378
+ class ValidateMacroSyntaxResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
379
+ """Syntax is valid."""
380
+
381
+ variables: list[VariableInfo]
382
+ warnings: list[str]
383
+
384
+
385
+ @dataclass
386
+ @PayloadRegistry.register
387
+ class ValidateMacroSyntaxResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
388
+ """Syntax is invalid."""
389
+
390
+ parse_failure: MacroParseFailure
391
+ partial_variables: list[VariableInfo]
392
+
393
+
394
+ @dataclass
395
+ @PayloadRegistry.register
396
+ class GetAllSituationsForProjectRequest(RequestPayload):
397
+ """Get all situation names and schemas from a project template."""
398
+
399
+ project_path: Path
400
+
401
+
402
+ @dataclass
403
+ @PayloadRegistry.register
404
+ class GetAllSituationsForProjectResultSuccess(WorkflowNotAlteredMixin, ResultPayloadSuccess):
405
+ """Success result containing all situations."""
406
+
407
+ situations: dict[str, str]
408
+
409
+
410
+ @dataclass
411
+ @PayloadRegistry.register
412
+ class GetAllSituationsForProjectResultFailure(WorkflowNotAlteredMixin, ResultPayloadFailure):
413
+ """Failure result when cannot get situations."""
@@ -10,6 +10,7 @@ from griptape_nodes.retained_mode.events.base_events import (
10
10
  WorkflowAlteredMixin,
11
11
  WorkflowNotAlteredMixin,
12
12
  )
13
+ from griptape_nodes.retained_mode.events.execution_events import ExecutionPayload
13
14
  from griptape_nodes.retained_mode.events.payload_registry import PayloadRegistry
14
15
 
15
16
  if TYPE_CHECKING:
@@ -388,9 +389,11 @@ class PublishWorkflowRequest(RequestPayload):
388
389
 
389
390
  workflow_name: str
390
391
  publisher_name: str
391
- execute_on_publish: bool = False
392
+ # This can be removed after GUI release
393
+ execute_on_publish: bool | None = None
392
394
  published_workflow_file_name: str | None = None
393
395
  pickle_control_flow_result: bool = False
396
+ metadata: dict | None = None
394
397
 
395
398
 
396
399
  @dataclass
@@ -403,6 +406,7 @@ class PublishWorkflowResultSuccess(ResultPayloadSuccess):
403
406
  """
404
407
 
405
408
  published_workflow_file_path: str
409
+ metadata: dict | None = None
406
410
 
407
411
 
408
412
  @dataclass
@@ -411,6 +415,20 @@ class PublishWorkflowResultFailure(ResultPayloadFailure):
411
415
  """Workflow publish failed. Common causes: workflow not found, publish error, file system error."""
412
416
 
413
417
 
418
+ @dataclass
419
+ @PayloadRegistry.register
420
+ class PublishWorkflowProgressEvent(ExecutionPayload):
421
+ """Event emitted to indicate progress during workflow publishing.
422
+
423
+ Args:
424
+ progress: Progress percentage (0-100)
425
+ message: Optional progress message
426
+ """
427
+
428
+ progress: float
429
+ message: str | None = None
430
+
431
+
414
432
  @dataclass
415
433
  @PayloadRegistry.register
416
434
  class BranchWorkflowRequest(RequestPayload):
@@ -51,6 +51,7 @@ if TYPE_CHECKING:
51
51
  OperationDepthManager,
52
52
  )
53
53
  from griptape_nodes.retained_mode.managers.os_manager import OSManager
54
+ from griptape_nodes.retained_mode.managers.project_manager import ProjectManager
54
55
  from griptape_nodes.retained_mode.managers.resource_manager import ResourceManager
55
56
  from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
56
57
  from griptape_nodes.retained_mode.managers.session_manager import SessionManager
@@ -95,8 +96,9 @@ class GriptapeNodes(metaclass=SingletonMeta):
95
96
  _resource_manager: ResourceManager
96
97
  _sync_manager: SyncManager
97
98
  _user_manager: UserManager
99
+ _project_manager: ProjectManager
98
100
 
99
- def __init__(self) -> None:
101
+ def __init__(self) -> None: # noqa: PLR0915
100
102
  from griptape_nodes.retained_mode.managers.agent_manager import AgentManager
101
103
  from griptape_nodes.retained_mode.managers.arbitrary_code_exec_manager import (
102
104
  ArbitraryCodeExecManager,
@@ -115,6 +117,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
115
117
  OperationDepthManager,
116
118
  )
117
119
  from griptape_nodes.retained_mode.managers.os_manager import OSManager
120
+ from griptape_nodes.retained_mode.managers.project_manager import ProjectManager
118
121
  from griptape_nodes.retained_mode.managers.resource_manager import ResourceManager
119
122
  from griptape_nodes.retained_mode.managers.secrets_manager import SecretsManager
120
123
  from griptape_nodes.retained_mode.managers.session_manager import SessionManager
@@ -160,6 +163,7 @@ class GriptapeNodes(metaclass=SingletonMeta):
160
163
  self._mcp_manager = MCPManager(self._event_manager, self._config_manager)
161
164
  self._sync_manager = SyncManager(self._event_manager, self._config_manager)
162
165
  self._user_manager = UserManager(self._secrets_manager)
166
+ self._project_manager = ProjectManager(self._event_manager, self._config_manager, self._secrets_manager)
163
167
 
164
168
  # Assign handlers now that these are created.
165
169
  self._event_manager.assign_manager_to_request_type(
@@ -330,6 +334,10 @@ class GriptapeNodes(metaclass=SingletonMeta):
330
334
  def UserManager(cls) -> UserManager:
331
335
  return GriptapeNodes.get_instance()._user_manager
332
336
 
337
+ @classmethod
338
+ def ProjectManager(cls) -> ProjectManager:
339
+ return GriptapeNodes.get_instance()._project_manager
340
+
333
341
  @classmethod
334
342
  def clear_data(cls) -> None:
335
343
  # Get canvas
@@ -7,11 +7,11 @@ import uuid
7
7
  from typing import TYPE_CHECKING, ClassVar
8
8
 
9
9
  from attrs import define, field
10
- from griptape.artifacts import ErrorArtifact, ImageUrlArtifact, JsonArtifact
10
+ from griptape.artifacts import ErrorArtifact, ImageUrlArtifact
11
11
  from griptape.drivers.image_generation import BaseImageGenerationDriver
12
12
  from griptape.drivers.image_generation.griptape_cloud import GriptapeCloudImageGenerationDriver
13
13
  from griptape.drivers.prompt.griptape_cloud import GriptapeCloudPromptDriver
14
- from griptape.events import FinishTaskEvent, TextChunkEvent
14
+ from griptape.events import TextChunkEvent
15
15
  from griptape.loaders import ImageLoader
16
16
  from griptape.memory.structure import ConversationMemory
17
17
  from griptape.rules import Rule, Ruleset
@@ -20,6 +20,7 @@ from griptape.tools import BaseImageGenerationTool
20
20
  from griptape.tools.mcp.tool import MCPTool
21
21
  from griptape.utils.decorators import activity
22
22
  from json_repair import repair_json
23
+ from pydantic import create_model
23
24
  from schema import Literal, Schema
24
25
 
25
26
  from griptape_nodes.retained_mode.events.agent_events import (
@@ -35,7 +36,6 @@ from griptape_nodes.retained_mode.events.agent_events import (
35
36
  ResetAgentConversationMemoryResultSuccess,
36
37
  RunAgentRequest,
37
38
  RunAgentResultFailure,
38
- RunAgentResultStarted,
39
39
  RunAgentResultSuccess,
40
40
  )
41
41
  from griptape_nodes.retained_mode.events.app_events import AppInitializationComplete
@@ -202,15 +202,17 @@ class AgentManager:
202
202
  self.image_tool = self._initialize_image_tool()
203
203
  if self.mcp_tool is None:
204
204
  self.mcp_tool = self._initialize_mcp_tool()
205
- await asyncio.to_thread(self._on_handle_run_agent_request, request)
206
- return RunAgentResultStarted(result_details="Agent execution started successfully.")
205
+ try:
206
+ return await asyncio.to_thread(self._on_handle_run_agent_request, request)
207
+ except Exception as e:
208
+ err_msg = f"Error handling run agent request: {e}"
209
+ return RunAgentResultFailure(error=ErrorArtifact(e).to_dict(), result_details=err_msg)
207
210
 
208
211
  def _create_agent(self, additional_mcp_servers: list[str] | None = None) -> Agent:
209
- output_schema = Schema(
210
- {
211
- "conversation_output": str,
212
- "generated_image_urls": [str],
213
- }
212
+ output_schema = create_model(
213
+ "AgentOutputSchema",
214
+ conversation_output=(str, ...),
215
+ generated_image_urls=(list[str], ...),
214
216
  )
215
217
 
216
218
  tools = []
@@ -252,7 +254,6 @@ class AgentManager:
252
254
  event_stream = agent.run_stream([request.input, *artifacts])
253
255
  full_result = ""
254
256
  last_conversation_output = ""
255
- last_event = None
256
257
  for event in event_stream:
257
258
  if isinstance(event, TextChunkEvent):
258
259
  full_result += event.token
@@ -274,19 +275,12 @@ class AgentManager:
274
275
  last_conversation_output = new_conversation_output
275
276
  except json.JSONDecodeError:
276
277
  pass # Ignore incomplete JSON
277
- last_event = event
278
- if isinstance(last_event, FinishTaskEvent):
279
- if isinstance(last_event.task_output, ErrorArtifact):
280
- return RunAgentResultFailure(
281
- error=last_event.task_output.to_dict(), result_details=last_event.task_output.to_json()
282
- )
283
- if isinstance(last_event.task_output, JsonArtifact):
284
- return RunAgentResultSuccess(
285
- last_event.task_output.to_dict(), result_details="Agent execution completed successfully."
286
- )
287
- err_msg = f"Unexpected final event: {last_event}"
288
- logger.error(err_msg)
289
- return RunAgentResultFailure(error=ErrorArtifact(last_event).to_dict(), result_details=err_msg)
278
+ if isinstance(agent.output, ErrorArtifact):
279
+ return RunAgentResultFailure(error=agent.output.to_dict(), result_details=agent.output.to_json())
280
+
281
+ return RunAgentResultSuccess(
282
+ agent.output.to_dict(), result_details="Agent execution completed successfully."
283
+ )
290
284
  except Exception as e:
291
285
  err_msg = f"Error running agent: {e}"
292
286
  logger.exception(err_msg)
@@ -7,6 +7,7 @@ from collections import defaultdict
7
7
  from dataclasses import fields
8
8
  from typing import TYPE_CHECKING, Any, cast
9
9
 
10
+ from asyncio_thread_runner import ThreadRunner
10
11
  from typing_extensions import TypedDict, TypeVar
11
12
 
12
13
  from griptape_nodes.retained_mode.events.base_events import (
@@ -276,8 +277,8 @@ class EventManager:
276
277
  if inspect.iscoroutinefunction(callback):
277
278
  try:
278
279
  asyncio.get_running_loop()
279
- msg = "Async handler cannot be called with sync handle_request. Use ahandle_request instead."
280
- raise ValueError(msg)
280
+ with ThreadRunner() as runner:
281
+ result_payload: ResultPayload = runner.run(callback(request))
281
282
  except RuntimeError:
282
283
  # No event loop running, safe to use asyncio.run
283
284
  result_payload: ResultPayload = asyncio.run(callback(request))
@@ -317,10 +318,6 @@ class EventManager:
317
318
  if app_event_type in self._app_event_listeners:
318
319
  listener_set = self._app_event_listeners[app_event_type]
319
320
 
320
- await asyncio.gather(
321
- *[
322
- asyncio.create_task(call_function(listener_callback, app_event))
323
- for listener_callback in listener_set
324
- ],
325
- return_exceptions=True,
326
- )
321
+ async with asyncio.TaskGroup() as tg:
322
+ for listener_callback in listener_set:
323
+ tg.create_task(call_function(listener_callback, app_event))