soe-ai 0.2.0b1__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 (145) hide show
  1. soe/__init__.py +50 -0
  2. soe/broker.py +168 -0
  3. soe/builtin_tools/__init__.py +51 -0
  4. soe/builtin_tools/soe_add_signal.py +82 -0
  5. soe/builtin_tools/soe_call_tool.py +111 -0
  6. soe/builtin_tools/soe_copy_context.py +80 -0
  7. soe/builtin_tools/soe_explore_docs.py +290 -0
  8. soe/builtin_tools/soe_get_available_tools.py +42 -0
  9. soe/builtin_tools/soe_get_context.py +50 -0
  10. soe/builtin_tools/soe_get_context_schema.py +56 -0
  11. soe/builtin_tools/soe_get_identities.py +63 -0
  12. soe/builtin_tools/soe_get_workflows.py +63 -0
  13. soe/builtin_tools/soe_inject_context_schema_field.py +80 -0
  14. soe/builtin_tools/soe_inject_identity.py +64 -0
  15. soe/builtin_tools/soe_inject_node.py +86 -0
  16. soe/builtin_tools/soe_inject_workflow.py +105 -0
  17. soe/builtin_tools/soe_list_contexts.py +73 -0
  18. soe/builtin_tools/soe_remove_context_schema_field.py +61 -0
  19. soe/builtin_tools/soe_remove_identity.py +61 -0
  20. soe/builtin_tools/soe_remove_node.py +72 -0
  21. soe/builtin_tools/soe_remove_workflow.py +62 -0
  22. soe/builtin_tools/soe_update_context.py +54 -0
  23. soe/docs/_config.yml +10 -0
  24. soe/docs/advanced_patterns/guide_fanout_and_aggregations.md +318 -0
  25. soe/docs/advanced_patterns/guide_inheritance.md +435 -0
  26. soe/docs/advanced_patterns/hybrid_intelligence.md +237 -0
  27. soe/docs/advanced_patterns/index.md +49 -0
  28. soe/docs/advanced_patterns/operational.md +781 -0
  29. soe/docs/advanced_patterns/self_evolving_workflows.md +385 -0
  30. soe/docs/advanced_patterns/swarm_intelligence.md +211 -0
  31. soe/docs/builtins/context.md +164 -0
  32. soe/docs/builtins/context_schema.md +158 -0
  33. soe/docs/builtins/identity.md +139 -0
  34. soe/docs/builtins/soe_explore_docs.md +135 -0
  35. soe/docs/builtins/tools.md +164 -0
  36. soe/docs/builtins/workflows.md +199 -0
  37. soe/docs/guide_00_getting_started.md +341 -0
  38. soe/docs/guide_01_tool.md +206 -0
  39. soe/docs/guide_02_llm.md +143 -0
  40. soe/docs/guide_03_router.md +146 -0
  41. soe/docs/guide_04_patterns.md +475 -0
  42. soe/docs/guide_05_agent.md +159 -0
  43. soe/docs/guide_06_schema.md +397 -0
  44. soe/docs/guide_07_identity.md +540 -0
  45. soe/docs/guide_08_child.md +612 -0
  46. soe/docs/guide_09_ecosystem.md +690 -0
  47. soe/docs/guide_10_infrastructure.md +427 -0
  48. soe/docs/guide_11_builtins.md +126 -0
  49. soe/docs/index.md +104 -0
  50. soe/docs/primitives/backends.md +281 -0
  51. soe/docs/primitives/context.md +256 -0
  52. soe/docs/primitives/node_reference.md +259 -0
  53. soe/docs/primitives/primitives.md +331 -0
  54. soe/docs/primitives/signals.md +865 -0
  55. soe/docs_index.py +2 -0
  56. soe/init.py +165 -0
  57. soe/lib/__init__.py +0 -0
  58. soe/lib/child_context.py +46 -0
  59. soe/lib/context_fields.py +51 -0
  60. soe/lib/inheritance.py +172 -0
  61. soe/lib/jinja_render.py +113 -0
  62. soe/lib/operational.py +51 -0
  63. soe/lib/parent_sync.py +71 -0
  64. soe/lib/register_event.py +75 -0
  65. soe/lib/schema_validation.py +134 -0
  66. soe/lib/yaml_parser.py +14 -0
  67. soe/local_backends/__init__.py +18 -0
  68. soe/local_backends/factory.py +124 -0
  69. soe/local_backends/in_memory/context.py +38 -0
  70. soe/local_backends/in_memory/conversation_history.py +60 -0
  71. soe/local_backends/in_memory/identity.py +52 -0
  72. soe/local_backends/in_memory/schema.py +40 -0
  73. soe/local_backends/in_memory/telemetry.py +38 -0
  74. soe/local_backends/in_memory/workflow.py +33 -0
  75. soe/local_backends/storage/context.py +57 -0
  76. soe/local_backends/storage/conversation_history.py +82 -0
  77. soe/local_backends/storage/identity.py +118 -0
  78. soe/local_backends/storage/schema.py +96 -0
  79. soe/local_backends/storage/telemetry.py +72 -0
  80. soe/local_backends/storage/workflow.py +56 -0
  81. soe/nodes/__init__.py +13 -0
  82. soe/nodes/agent/__init__.py +10 -0
  83. soe/nodes/agent/factory.py +134 -0
  84. soe/nodes/agent/lib/loop_handlers.py +150 -0
  85. soe/nodes/agent/lib/loop_state.py +157 -0
  86. soe/nodes/agent/lib/prompts.py +65 -0
  87. soe/nodes/agent/lib/tools.py +35 -0
  88. soe/nodes/agent/stages/__init__.py +12 -0
  89. soe/nodes/agent/stages/parameter.py +37 -0
  90. soe/nodes/agent/stages/response.py +54 -0
  91. soe/nodes/agent/stages/router.py +37 -0
  92. soe/nodes/agent/state.py +111 -0
  93. soe/nodes/agent/types.py +66 -0
  94. soe/nodes/agent/validation/__init__.py +11 -0
  95. soe/nodes/agent/validation/config.py +95 -0
  96. soe/nodes/agent/validation/operational.py +24 -0
  97. soe/nodes/child/__init__.py +3 -0
  98. soe/nodes/child/factory.py +61 -0
  99. soe/nodes/child/state.py +59 -0
  100. soe/nodes/child/validation/__init__.py +11 -0
  101. soe/nodes/child/validation/config.py +126 -0
  102. soe/nodes/child/validation/operational.py +28 -0
  103. soe/nodes/lib/conditions.py +71 -0
  104. soe/nodes/lib/context.py +24 -0
  105. soe/nodes/lib/conversation_history.py +77 -0
  106. soe/nodes/lib/identity.py +64 -0
  107. soe/nodes/lib/llm_resolver.py +142 -0
  108. soe/nodes/lib/output.py +68 -0
  109. soe/nodes/lib/response_builder.py +91 -0
  110. soe/nodes/lib/signal_emission.py +79 -0
  111. soe/nodes/lib/signals.py +54 -0
  112. soe/nodes/lib/tools.py +100 -0
  113. soe/nodes/llm/__init__.py +7 -0
  114. soe/nodes/llm/factory.py +103 -0
  115. soe/nodes/llm/state.py +76 -0
  116. soe/nodes/llm/types.py +12 -0
  117. soe/nodes/llm/validation/__init__.py +11 -0
  118. soe/nodes/llm/validation/config.py +89 -0
  119. soe/nodes/llm/validation/operational.py +23 -0
  120. soe/nodes/router/__init__.py +3 -0
  121. soe/nodes/router/factory.py +37 -0
  122. soe/nodes/router/state.py +32 -0
  123. soe/nodes/router/validation/__init__.py +11 -0
  124. soe/nodes/router/validation/config.py +58 -0
  125. soe/nodes/router/validation/operational.py +16 -0
  126. soe/nodes/tool/factory.py +66 -0
  127. soe/nodes/tool/lib/__init__.py +11 -0
  128. soe/nodes/tool/lib/conditions.py +35 -0
  129. soe/nodes/tool/lib/failure.py +28 -0
  130. soe/nodes/tool/lib/parameters.py +67 -0
  131. soe/nodes/tool/state.py +66 -0
  132. soe/nodes/tool/types.py +27 -0
  133. soe/nodes/tool/validation/__init__.py +15 -0
  134. soe/nodes/tool/validation/config.py +132 -0
  135. soe/nodes/tool/validation/operational.py +16 -0
  136. soe/types.py +209 -0
  137. soe/validation/__init__.py +18 -0
  138. soe/validation/config.py +195 -0
  139. soe/validation/jinja.py +54 -0
  140. soe/validation/operational.py +110 -0
  141. soe_ai-0.2.0b1.dist-info/METADATA +262 -0
  142. soe_ai-0.2.0b1.dist-info/RECORD +145 -0
  143. soe_ai-0.2.0b1.dist-info/WHEEL +5 -0
  144. soe_ai-0.2.0b1.dist-info/licenses/LICENSE +21 -0
  145. soe_ai-0.2.0b1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,865 @@
1
+
2
+ # Appendix C: Signals Reference
3
+
4
+ This appendix covers everything about signals in SOE: how they trigger nodes, how they're emitted, and the condition evaluation rules for each node type.
5
+
6
+ All examples are tested. Run them with:
7
+
8
+ ```bash
9
+ uv run pytest tests/test_cases/appendix/c_signals/ -v
10
+ ```
11
+
12
+ ---
13
+
14
+ ## What Are Signals?
15
+
16
+ Signals are the **communication mechanism** between nodes in SOE. They:
17
+
18
+ - **Trigger** nodes via `event_triggers`
19
+ - **Route** execution flow between nodes
20
+ - **Propagate** to parent workflows in sub-orchestration
21
+
22
+ Think of signals like events in a pub/sub system. Nodes subscribe to signals they care about, and emit signals to notify other nodes.
23
+
24
+ ---
25
+
26
+ ## Signal Naming Best Practices
27
+
28
+ ### Use Descriptive, Action-Oriented Names
29
+
30
+ ```yaml
31
+ # ✅ Good - clear intent
32
+ event_emissions:
33
+ - signal_name: ANALYSIS_COMPLETE
34
+ - signal_name: VALIDATION_FAILED
35
+ - signal_name: USER_VERIFIED
36
+
37
+ # ❌ Bad - vague or generic
38
+ event_emissions:
39
+ - signal_name: DONE
40
+ - signal_name: NEXT
41
+ - signal_name: STEP_2
42
+ ```
43
+
44
+ ### Prefix Workflow-Specific Signals
45
+
46
+ When using sub-orchestration, prefix signals with the workflow name for clarity:
47
+
48
+ ```yaml
49
+ # Parent workflow
50
+ signals_to_parent: [ANALYSIS_COMPLETE] # Clear origin
51
+
52
+ # Instead of
53
+ signals_to_parent: [DONE] # What finished? Unclear to parent
54
+ ```
55
+
56
+ ### Use Consistent Conventions
57
+
58
+ | Convention | Example | Use Case |
59
+ |------------|---------|----------|
60
+ | `*_COMPLETE` | `ANALYSIS_COMPLETE` | Successful completion |
61
+ | `*_FAILED` | `VALIDATION_FAILED` | Error states |
62
+ | `*_READY` | `DATA_READY` | Readiness signals |
63
+ | `*_REQUIRED` | `REVIEW_REQUIRED` | Action needed |
64
+
65
+ ---
66
+
67
+ ## The `event_emissions` Field
68
+
69
+ Every node type can emit signals via `event_emissions`:
70
+
71
+ ```yaml
72
+ event_emissions:
73
+ - signal_name: SUCCESS
74
+ - signal_name: NEEDS_REVIEW
75
+ condition: "{{ result.confidence < 0.8 }}"
76
+ ```
77
+
78
+ ### Fields
79
+
80
+ | Field | Type | Required | Description |
81
+ |-------|------|----------|-------------|
82
+ | `signal_name` | `str` | Yes | The signal to emit |
83
+ | `condition` | `str` | No | Controls when the signal is emitted |
84
+
85
+ ### No Signals (Terminal Node)
86
+
87
+ A node with empty or missing `event_emissions` is a **terminal node**—it executes but emits nothing:
88
+
89
+ ```yaml
90
+ LogAndFinish:
91
+ node_type: tool
92
+ event_triggers: [COMPLETE]
93
+ tool_name: log_result
94
+ input_fields: [result]
95
+ # No event_emissions = terminal node
96
+ ```
97
+
98
+ Use this pattern for:
99
+ - Final logging/cleanup nodes
100
+ - Fire-and-forget operations
101
+ - Workflow endpoints
102
+
103
+ ---
104
+
105
+ ## Condition Types: The Three Modes
106
+
107
+ The `condition` field has **three modes** that determine when and how signals are emitted:
108
+
109
+ ### Mode 1: No Condition (Unconditional)
110
+
111
+ Signals without conditions **always emit** after node execution:
112
+
113
+ ```yaml
114
+ example_workflow:
115
+ TriggerMultiple:
116
+ node_type: router
117
+ event_triggers: [START]
118
+ event_emissions:
119
+ - signal_name: PROCESSING_DONE
120
+ - signal_name: LOG_EVENT
121
+ ```
122
+
123
+ Both `PROCESSING_DONE` and `LOG_EVENT` emit every time the node runs.
124
+
125
+ > **Note**: For Router nodes, multiple unconditional signals all emit simultaneously (fan-out pattern). For LLM/Agent nodes with multiple signals, the LLM must select one - use Jinja conditions like `{{ true }}` if you want all signals to emit.
126
+
127
+ ### Mode 2: Jinja Template (Programmatic)
128
+
129
+ Conditions containing `{{ }}` are evaluated programmatically:
130
+
131
+ ```yaml
132
+ example_workflow:
133
+ AnalyzeAndRoute:
134
+ node_type: llm
135
+ event_triggers: [START]
136
+ prompt: "Analyze: &#123;&#123; context.text &#125;&#125;"
137
+ output_field: analysis
138
+ event_emissions:
139
+ - signal_name: HIGH_PRIORITY
140
+ condition: "&#123;&#123; context.priority > 5 &#125;&#125;"
141
+ - signal_name: NORMAL_PRIORITY
142
+ condition: "&#123;&#123; context.priority <= 5 &#125;&#125;"
143
+ ```
144
+
145
+ SOE evaluates `context.priority > 5` and emits the matching signal. No LLM involvement.
146
+
147
+ ### Mode 3: Plain Text (Semantic/LLM Selection)
148
+
149
+ Plain text conditions trigger LLM signal selection:
150
+
151
+ ```yaml
152
+ example_workflow:
153
+ SentimentRouter:
154
+ node_type: llm
155
+ event_triggers: [START]
156
+ prompt: "Analyze the sentiment of this message: &#123;&#123; context.message &#125;&#125;"
157
+ output_field: sentiment_analysis
158
+ event_emissions:
159
+ - signal_name: POSITIVE_SENTIMENT
160
+ condition: "The message expresses happiness, satisfaction, or positive emotions"
161
+ - signal_name: NEGATIVE_SENTIMENT
162
+ condition: "The message expresses anger, frustration, or negative emotions"
163
+ - signal_name: NEUTRAL_SENTIMENT
164
+ condition: "The message is factual, neutral, or emotionally ambiguous"
165
+ ```
166
+
167
+ SOE asks the LLM: "Based on your analysis, which signal should be emitted?" The LLM chooses based on semantic understanding.
168
+
169
+ ---
170
+
171
+ ## How SOE Decides: The Decision Tree
172
+
173
+ The behavior depends on the node type:
174
+
175
+ ### Router Node
176
+
177
+ ```
178
+
179
+ ┌─────────────────────────────────────────────────────────────┐
180
+ │ Router Signal Emission │
181
+ ├─────────────────────────────────────────────────────────────┤
182
+ │ │
183
+ │ For EACH signal in event_emissions: │
184
+ │ └─ No condition? → Emit │
185
+ │ └─ Jinja condition? → Evaluate, emit if truthy │
186
+ │ │
187
+ │ → Multiple signals can emit (fan-out pattern) │
188
+ │ │
189
+ └─────────────────────────────────────────────────────────────┘
190
+
191
+ ```
192
+
193
+ ### LLM/Agent Node
194
+
195
+ ```
196
+
197
+ ┌─────────────────────────────────────────────────────────────┐
198
+ │ LLM/Agent Signal Emission │
199
+ ├─────────────────────────────────────────────────────────────┤
200
+ │ │
201
+ │ 1. Any condition contains {{ }}? │
202
+ │ └─ YES → Evaluate ALL conditions programmatically │
203
+ │ Emit signals where condition is truthy │
204
+ │ └─ NO → Continue to step 2 │
205
+ │ │
206
+ │ 2. Count signals │
207
+ │ └─ Zero signals? → Nothing emitted │
208
+ │ └─ Single signal? → Emit unconditionally │
209
+ │ └─ Multiple signals? │
210
+ │ └─ LLM selects ONE signal │
211
+ │ (uses conditions as semantic descriptions) │
212
+ │ │
213
+ └─────────────────────────────────────────────────────────────┘
214
+
215
+ ```
216
+
217
+ ### Critical Rule: Jinja Takes Over
218
+
219
+ If **any** condition contains `{{ }}`, ALL conditions are evaluated programmatically. The LLM never selects signals when Jinja is present.
220
+
221
+ ---
222
+
223
+ ## Signal Behavior by Node Type
224
+
225
+ ### Router Node
226
+
227
+ **Purpose**: Conditional branching based on context.
228
+
229
+ **Condition Context**: `context` only.
230
+
231
+ **Evaluation**: Always programmatic (Jinja). Plain text conditions are not LLM-selected.
232
+
233
+ ```yaml
234
+ example_workflow:
235
+ ValidateInput:
236
+ node_type: router
237
+ event_triggers: [START]
238
+ event_emissions:
239
+ - signal_name: HAS_DATA
240
+ condition: "&#123;&#123; context.data is defined and context.data &#125;&#125;"
241
+ - signal_name: NO_DATA
242
+ condition: "&#123;&#123; context.data is not defined or not context.data &#125;&#125;"
243
+
244
+ ProcessData:
245
+ node_type: router
246
+ event_triggers: [HAS_DATA]
247
+ event_emissions:
248
+ - signal_name: DONE
249
+
250
+ HandleMissing:
251
+ node_type: router
252
+ event_triggers: [NO_DATA]
253
+ event_emissions:
254
+ - signal_name: DONE
255
+ ```
256
+
257
+ **Key Point**: Router conditions must use Jinja. Plain text won't work as expected.
258
+
259
+ ---
260
+
261
+ ### LLM Node
262
+
263
+ **Purpose**: Single LLM call with optional signal selection.
264
+
265
+ **Condition Context**: `context` only (LLM output stored in `output_field`).
266
+
267
+ **Evaluation**: Jinja → programmatic. Plain text → LLM selection.
268
+
269
+ **Jinja Example** (SOE evaluates):
270
+
271
+ ```yaml
272
+ example_workflow:
273
+ AnalyzeAndRoute:
274
+ node_type: llm
275
+ event_triggers: [START]
276
+ prompt: "Analyze: &#123;&#123; context.text &#125;&#125;"
277
+ output_field: analysis
278
+ event_emissions:
279
+ - signal_name: HIGH_PRIORITY
280
+ condition: "&#123;&#123; context.priority > 5 &#125;&#125;"
281
+ - signal_name: NORMAL_PRIORITY
282
+ condition: "&#123;&#123; context.priority <= 5 &#125;&#125;"
283
+ ```
284
+
285
+ **Plain Text Example** (LLM selects):
286
+
287
+ ```yaml
288
+ example_workflow:
289
+ SentimentRouter:
290
+ node_type: llm
291
+ event_triggers: [START]
292
+ prompt: "Analyze the sentiment of this message: &#123;&#123; context.message &#125;&#125;"
293
+ output_field: sentiment_analysis
294
+ event_emissions:
295
+ - signal_name: POSITIVE_SENTIMENT
296
+ condition: "The message expresses happiness, satisfaction, or positive emotions"
297
+ - signal_name: NEGATIVE_SENTIMENT
298
+ condition: "The message expresses anger, frustration, or negative emotions"
299
+ - signal_name: NEUTRAL_SENTIMENT
300
+ condition: "The message is factual, neutral, or emotionally ambiguous"
301
+ ```
302
+
303
+ **LLM Selection Mechanism**: SOE adds a `selected_signal` field to the response model, forcing the LLM to choose from the options. The condition text serves as the description.
304
+
305
+ ---
306
+
307
+ ### Agent Node
308
+
309
+ **Purpose**: ReAct loop with tool access.
310
+
311
+ **Condition Context**: `context` only.
312
+
313
+ **Evaluation**: Same as LLM node—Jinja → programmatic, plain text → LLM selection.
314
+
315
+ ```yaml
316
+ example_workflow:
317
+ TaskAgent:
318
+ node_type: agent
319
+ event_triggers: [START]
320
+ prompt: "Help the user with their request: &#123;&#123; context.request &#125;&#125;"
321
+ available_tools: [search, calculate]
322
+ output_field: result
323
+ event_emissions:
324
+ - signal_name: TASK_COMPLETED
325
+ condition: "The task was successfully completed with a satisfactory result"
326
+ - signal_name: TASK_FAILED
327
+ condition: "The task could not be completed due to limitations or errors"
328
+ - signal_name: TASK_NEEDS_CLARIFICATION
329
+ condition: "The request is ambiguous and needs more information from the user"
330
+ ```
331
+
332
+ **Note**: Agent tools are selected by the LLM within the ReAct loop. Signal selection is a separate decision made after the agent loop completes.
333
+
334
+ ---
335
+
336
+ ### Tool Node
337
+
338
+ **Purpose**: Direct function execution.
339
+
340
+ **Condition Context**: `result` AND `context`.
341
+
342
+ **Evaluation**: Always programmatic. No LLM selection (tools don't call LLMs).
343
+
344
+ ```yaml
345
+ example_workflow:
346
+ ProcessPayment:
347
+ node_type: tool
348
+ event_triggers: [START]
349
+ tool_name: process_payment
350
+ context_parameter_field: payment_data
351
+ output_field: payment_result
352
+ event_emissions:
353
+ - signal_name: PAYMENT_APPROVED
354
+ condition: "&#123;&#123; result.status == 'approved' &#125;&#125;"
355
+ - signal_name: PAYMENT_DECLINED
356
+ condition: "&#123;&#123; result.status == 'declined' &#125;&#125;"
357
+ - signal_name: PAYMENT_PENDING
358
+ condition: "&#123;&#123; result.status == 'pending' &#125;&#125;"
359
+
360
+ OnApproved:
361
+ node_type: router
362
+ event_triggers: [PAYMENT_APPROVED]
363
+ event_emissions:
364
+ - signal_name: DONE
365
+
366
+ OnDeclined:
367
+ node_type: router
368
+ event_triggers: [PAYMENT_DECLINED]
369
+ event_emissions:
370
+ - signal_name: DONE
371
+
372
+ OnPending:
373
+ node_type: router
374
+ event_triggers: [PAYMENT_PENDING]
375
+ event_emissions:
376
+ - signal_name: DONE
377
+ ```
378
+
379
+ #### The `result` Keyword
380
+
381
+ Tool nodes have a special `result` variable in condition evaluation:
382
+
383
+ | Variable | Description | Example |
384
+ |----------|-------------|---------|
385
+ | `result` | The return value of the tool function | `{{ result.status }}` |
386
+ | `context` | The execution context | `{{ context.user_id }}` |
387
+
388
+ **Combining result and context**:
389
+
390
+ ```yaml
391
+ example_workflow:
392
+ CheckOrder:
393
+ node_type: tool
394
+ event_triggers: [START]
395
+ tool_name: validate_order
396
+ context_parameter_field: order
397
+ output_field: validation
398
+ event_emissions:
399
+ - signal_name: VIP_LARGE_ORDER
400
+ condition: "&#123;&#123; result.valid and context.customer.is_vip and context.order.total > 1000 &#125;&#125;"
401
+ - signal_name: VIP_ORDER
402
+ condition: "&#123;&#123; result.valid and context.customer.is_vip &#125;&#125;"
403
+ - signal_name: LARGE_ORDER
404
+ condition: "&#123;&#123; result.valid and context.order.total > 1000 &#125;&#125;"
405
+ - signal_name: STANDARD_ORDER
406
+ condition: "&#123;&#123; result.valid &#125;&#125;"
407
+ - signal_name: INVALID_ORDER
408
+ condition: "&#123;&#123; not result.valid &#125;&#125;"
409
+ ```
410
+
411
+ **Why `result`?**: Tools return values that aren't stored in context until after condition evaluation. The `result` keyword provides access to the raw return value.
412
+
413
+ ---
414
+
415
+ ### Child Node
416
+
417
+ **Purpose**: Sub-orchestration.
418
+
419
+ **Condition Context**: `context` only.
420
+
421
+ **Evaluation**: Always programmatic.
422
+
423
+ **Note**: Child node `event_emissions` fire after the child workflow **starts**, not when it completes. Use `signals_to_parent` to get completion signals.
424
+
425
+ ---
426
+
427
+ ## Failure Signals
428
+
429
+ LLM, Agent, and Tool nodes can emit **failure signals** when they fail after exhausting retries. This enables graceful error handling.
430
+
431
+ ### LLM Failure Signal
432
+
433
+ ```yaml
434
+ example_workflow:
435
+ RiskyLLMCall:
436
+ node_type: llm
437
+ event_triggers: [START]
438
+ prompt: "Generate a complex response for: &#123;&#123; context.input &#125;&#125;"
439
+ output_field: response
440
+ retries: 2
441
+ llm_failure_signal: LLM_FAILED
442
+ event_emissions:
443
+ - signal_name: SUCCESS
444
+
445
+ HandleSuccess:
446
+ node_type: router
447
+ event_triggers: [SUCCESS]
448
+ event_emissions:
449
+ - signal_name: WORKFLOW_COMPLETE
450
+
451
+ HandleFailure:
452
+ node_type: router
453
+ event_triggers: [LLM_FAILED]
454
+ event_emissions:
455
+ - signal_name: WORKFLOW_COMPLETE
456
+ ```
457
+
458
+ When the LLM fails after 2 retries (3 total attempts), `LLM_FAILED` is emitted instead of `SUCCESS`.
459
+
460
+ ### Agent Failure Signal
461
+
462
+ ```yaml
463
+ example_workflow:
464
+ ComplexAgent:
465
+ node_type: agent
466
+ event_triggers: [START]
467
+ prompt: "Complete this complex task: &#123;&#123; context.task &#125;&#125;"
468
+ available_tools: [search]
469
+ output_field: result
470
+ retries: 1
471
+ llm_failure_signal: AGENT_EXHAUSTED
472
+ event_emissions:
473
+ - signal_name: TASK_DONE
474
+
475
+ OnSuccess:
476
+ node_type: router
477
+ event_triggers: [TASK_DONE]
478
+ event_emissions:
479
+ - signal_name: DONE
480
+
481
+ OnFailure:
482
+ node_type: router
483
+ event_triggers: [AGENT_EXHAUSTED]
484
+ event_emissions:
485
+ - signal_name: DONE
486
+ ```
487
+
488
+ ### Tool Failure Signal (Registry-Based)
489
+
490
+ Unlike LLM/Agent nodes, tool failure signals are configured in the **tools registry**, not in the YAML workflow:
491
+
492
+ ```yaml
493
+ example_workflow:
494
+ CallExternalAPI:
495
+ node_type: tool
496
+ event_triggers: [START]
497
+ tool_name: flaky_api
498
+ context_parameter_field: api_params
499
+ output_field: api_result
500
+ event_emissions:
501
+ - signal_name: API_SUCCESS
502
+
503
+ OnSuccess:
504
+ node_type: router
505
+ event_triggers: [API_SUCCESS]
506
+ event_emissions:
507
+ - signal_name: DONE
508
+
509
+ OnFailure:
510
+ node_type: router
511
+ event_triggers: [API_FAILED]
512
+ event_emissions:
513
+ - signal_name: DONE
514
+ ```
515
+
516
+ The `failure_signal` is configured when creating the tools registry:
517
+
518
+ ```python
519
+ tools_registry = {
520
+ "flaky_api": {
521
+ "function": flaky_api,
522
+ "max_retries": 2, # Retry up to 2 times after initial failure
523
+ "failure_signal": "API_FAILED", # Emit when all retries exhausted
524
+ }
525
+ }
526
+ ```
527
+
528
+ When `flaky_api` throws an exception and exhausts all retries, `API_FAILED` is emitted.
529
+
530
+ ### Failure Signal Behavior
531
+
532
+ | Field | Node Type | Location | Description |
533
+ |-------|-----------|----------|-------------|
534
+ | `llm_failure_signal` | LLM, Agent | YAML workflow | Signal emitted when LLM call fails after retries |
535
+ | `retries` | LLM, Agent | YAML workflow | Number of retry attempts (default: 3) |
536
+ | `failure_signal` | Tool | Tools registry | Signal emitted when tool fails after retries |
537
+ | `max_retries` | Tool | Tools registry | Number of retry attempts (default: 1) |
538
+
539
+ **Key Points**:
540
+ - Failure signals are **only emitted** if configured
541
+ - They replace normal `event_emissions` on failure
542
+ - Use them to create error handling branches in your workflow
543
+ - Without a failure signal, failures raise exceptions
544
+ - **Tool failure signals** are in the registry because tools are Python functions with no YAML config
545
+
546
+ ---
547
+
548
+ ## Sub-Orchestration Signal Propagation
549
+
550
+ ### The `signals_to_parent` Field
551
+
552
+ Controls which child signals propagate to the parent workflow:
553
+
554
+ ```yaml
555
+ parent_workflow:
556
+ StartAnalysis:
557
+ node_type: child
558
+ event_triggers: [START]
559
+ child_workflow_name: analysis_child
560
+ child_initial_signals: [BEGIN]
561
+ input_fields: [data_to_analyze]
562
+ signals_to_parent: [ANALYSIS_SUCCESS, ANALYSIS_FAILED]
563
+ context_updates_to_parent: [analysis_result]
564
+
565
+ OnSuccess:
566
+ node_type: router
567
+ event_triggers: [ANALYSIS_SUCCESS]
568
+ event_emissions:
569
+ - signal_name: PARENT_DONE
570
+
571
+ OnFailure:
572
+ node_type: router
573
+ event_triggers: [ANALYSIS_FAILED]
574
+ event_emissions:
575
+ - signal_name: PARENT_DONE
576
+
577
+ analysis_child:
578
+ Analyze:
579
+ node_type: llm
580
+ event_triggers: [BEGIN]
581
+ prompt: "Analyze this data: &#123;&#123; context.data_to_analyze &#125;&#125;"
582
+ output_field: analysis_result
583
+ event_emissions:
584
+ - signal_name: ANALYSIS_SUCCESS
585
+ condition: "Analysis completed successfully"
586
+ - signal_name: ANALYSIS_FAILED
587
+ condition: "Analysis could not be completed"
588
+ ```
589
+
590
+ Only `ANALYSIS_SUCCESS` and `ANALYSIS_FAILED` reach the parent. Other child signals stay internal.
591
+
592
+ ### Best Practices for Sub-Orchestration Signals
593
+
594
+ #### 1. Use Specific, Workflow-Prefixed Names
595
+
596
+ ```yaml
597
+ # ✅ Good - parent knows exactly what completed
598
+ signals_to_parent: [ANALYZER_COMPLETE, ANALYZER_FAILED]
599
+
600
+ # ❌ Bad - ambiguous in parent context
601
+ signals_to_parent: [DONE, ERROR]
602
+ ```
603
+
604
+ #### 2. Keep Internal Signals Internal
605
+
606
+ Don't propagate signals that only matter within the child.
607
+
608
+ #### 3. Consider the Parent's Perspective
609
+
610
+ The parent workflow should receive signals that are actionable.
611
+
612
+ ---
613
+
614
+ ## The `context_updates_to_parent` Field
615
+
616
+ Controls which context keys are synced to the parent:
617
+
618
+ ```yaml
619
+ SpawnAnalyzer:
620
+ node_type: child
621
+ context_updates_to_parent: [analysis_result, confidence_score]
622
+ ```
623
+
624
+ When the child updates these keys, they're automatically copied to the parent's context.
625
+
626
+ ---
627
+
628
+ ## LLM Signal Selection: Under the Hood
629
+
630
+ When the LLM selects a signal, SOE:
631
+
632
+ 1. **Builds a response model** with a `selected_signal` field:
633
+ ```python
634
+ class Response(BaseModel):
635
+ response: str
636
+ selected_signal: Literal["POSITIVE", "NEGATIVE", "NEUTRAL"]
637
+ ```
638
+
639
+ 2. **Provides descriptions** from the `condition` field:
640
+ ```
641
+ Select one of these signals based on your response:
642
+ - POSITIVE: The message expresses positive sentiment
643
+ - NEGATIVE: The message expresses negative sentiment
644
+ - NEUTRAL: The message is neutral
645
+ ```
646
+
647
+ 3. **Extracts the selection** and emits that signal.
648
+
649
+ This is why plain-text conditions are called "semantic"—the LLM understands the description and makes a judgment call.
650
+
651
+ ---
652
+
653
+ ## Common Patterns
654
+
655
+ ### Exclusive Routing
656
+
657
+ Only one path taken based on mutually exclusive conditions:
658
+
659
+ ```yaml
660
+ example_workflow:
661
+ RouteByType:
662
+ node_type: router
663
+ event_triggers: [START]
664
+ event_emissions:
665
+ - signal_name: TYPE_A
666
+ condition: "&#123;&#123; context.type == 'a' &#125;&#125;"
667
+ - signal_name: TYPE_B
668
+ condition: "&#123;&#123; context.type == 'b' &#125;&#125;"
669
+ - signal_name: TYPE_DEFAULT
670
+ condition: "&#123;&#123; context.type not in ['a', 'b'] &#125;&#125;"
671
+
672
+ HandleA:
673
+ node_type: router
674
+ event_triggers: [TYPE_A]
675
+ event_emissions:
676
+ - signal_name: DONE
677
+
678
+ HandleB:
679
+ node_type: router
680
+ event_triggers: [TYPE_B]
681
+ event_emissions:
682
+ - signal_name: DONE
683
+
684
+ HandleDefault:
685
+ node_type: router
686
+ event_triggers: [TYPE_DEFAULT]
687
+ event_emissions:
688
+ - signal_name: DONE
689
+ ```
690
+
691
+ ### Fan-Out (Multiple Signals)
692
+
693
+ Multiple signals can emit simultaneously, triggering parallel processing:
694
+
695
+ ```yaml
696
+ example_workflow:
697
+ TriggerMultiple:
698
+ node_type: router
699
+ event_triggers: [START]
700
+ event_emissions:
701
+ - signal_name: NOTIFY_USER
702
+ condition: "&#123;&#123; context.notify_user &#125;&#125;"
703
+ - signal_name: LOG_EVENT
704
+ condition: "&#123;&#123; context.log_enabled &#125;&#125;"
705
+ - signal_name: UPDATE_METRICS
706
+
707
+ NotifyUser:
708
+ node_type: router
709
+ event_triggers: [NOTIFY_USER]
710
+ event_emissions:
711
+ - signal_name: NOTIFICATION_SENT
712
+
713
+ LogEvent:
714
+ node_type: router
715
+ event_triggers: [LOG_EVENT]
716
+ event_emissions:
717
+ - signal_name: EVENT_LOGGED
718
+
719
+ UpdateMetrics:
720
+ node_type: router
721
+ event_triggers: [UPDATE_METRICS]
722
+ event_emissions:
723
+ - signal_name: METRICS_UPDATED
724
+ ```
725
+
726
+ ---
727
+
728
+ ## Complete Example: Order Processing
729
+
730
+ This workflow demonstrates multiple signal patterns working together:
731
+
732
+ ```yaml
733
+ order_processing:
734
+ # 1. Router with Jinja conditions (programmatic)
735
+ ValidateOrder:
736
+ node_type: router
737
+ event_triggers: [START]
738
+ event_emissions:
739
+ - signal_name: ORDER_VALID
740
+ condition: "&#123;&#123; context.order['line_items']|length > 0 and context.order.total > 0 &#125;&#125;"
741
+ - signal_name: ORDER_INVALID
742
+ condition: "&#123;&#123; context.order['line_items']|length == 0 or context.order.total <= 0 &#125;&#125;"
743
+
744
+ # 2. Tool with result conditions
745
+ ProcessPayment:
746
+ node_type: tool
747
+ event_triggers: [ORDER_VALID]
748
+ tool_name: charge_card
749
+ context_parameter_field: payment_info
750
+ output_field: payment_result
751
+ event_emissions:
752
+ - signal_name: PAYMENT_SUCCESS
753
+ condition: "&#123;&#123; result.charged == true &#125;&#125;"
754
+ - signal_name: PAYMENT_FAILED
755
+ condition: "&#123;&#123; result.charged == false &#125;&#125;"
756
+
757
+ # 3. LLM with failure signal and unconditional emission
758
+ GenerateConfirmation:
759
+ node_type: llm
760
+ event_triggers: [PAYMENT_SUCCESS]
761
+ prompt: "Generate a friendly order confirmation for order #&#123;&#123; context.order.id &#125;&#125;"
762
+ output_field: confirmation_message
763
+ retries: 2
764
+ llm_failure_signal: CONFIRMATION_FAILED
765
+ event_emissions:
766
+ - signal_name: ORDER_COMPLETE
767
+
768
+ # 4. Fan-out: notify multiple systems
769
+ NotifySystems:
770
+ node_type: router
771
+ event_triggers: [ORDER_COMPLETE]
772
+ event_emissions:
773
+ - signal_name: NOTIFY_CUSTOMER
774
+ - signal_name: UPDATE_INVENTORY
775
+ - signal_name: LOG_ORDER
776
+
777
+ # Handle failures
778
+ HandleInvalidOrder:
779
+ node_type: router
780
+ event_triggers: [ORDER_INVALID]
781
+ event_emissions:
782
+ - signal_name: WORKFLOW_ERROR
783
+
784
+ HandlePaymentFailed:
785
+ node_type: router
786
+ event_triggers: [PAYMENT_FAILED]
787
+ event_emissions:
788
+ - signal_name: WORKFLOW_ERROR
789
+
790
+ HandleConfirmationFailed:
791
+ node_type: router
792
+ event_triggers: [CONFIRMATION_FAILED]
793
+ event_emissions:
794
+ - signal_name: ORDER_COMPLETE
795
+ ```
796
+
797
+ **Patterns demonstrated**:
798
+ 1. **Jinja conditions** (Router): `ORDER_VALID` vs `ORDER_INVALID`
799
+ 2. **Tool result conditions**: `PAYMENT_SUCCESS` vs `PAYMENT_FAILED`
800
+ 3. **Failure signal**: `CONFIRMATION_FAILED` on LLM error
801
+ 4. **Fan-out**: `NOTIFY_CUSTOMER`, `UPDATE_INVENTORY`, `LOG_ORDER` emit together
802
+
803
+ ---
804
+
805
+ ## Debugging Signal Issues
806
+
807
+ ### Signal Not Emitting
808
+
809
+ 1. **Check condition syntax**: Is the Jinja valid?
810
+ 2. **Check condition logic**: Does it evaluate to truthy?
811
+ 3. **Check for typos**: Signal names are case-sensitive.
812
+
813
+ ### Wrong Signal Emitting
814
+
815
+ 1. **Jinja vs plain text**: Did you mean LLM selection but used Jinja?
816
+ 2. **Missing condition**: Signals without conditions always emit.
817
+ 3. **Multiple matches**: Multiple conditions can be truthy simultaneously.
818
+
819
+ ### Jinja Attribute Access Gotcha
820
+
821
+ When accessing dict keys in Jinja, be careful with keys that conflict with dict methods:
822
+
823
+ ```yaml
824
+ # ❌ BAD: 'items' conflicts with dict.items() method
825
+ condition: "{{ context.order.items|length > 0 }}"
826
+
827
+ # ✅ GOOD: Use bracket notation for conflicting keys
828
+ condition: "{{ context.order['items']|length > 0 }}"
829
+
830
+ # ✅ GOOD: Rename to avoid conflicts
831
+ condition: "{{ context.order.line_items|length > 0 }}"
832
+ ```
833
+
834
+ **Conflicting dict method names to avoid**: `items`, `keys`, `values`, `get`, `pop`, `update`
835
+
836
+ ### Parent Not Receiving Signal
837
+
838
+ 1. **Check `signals_to_parent`**: Is the signal listed?
839
+ 2. **Check signal name match**: Exact string match required.
840
+ 3. **Verify child emitted**: Debug the child workflow first.
841
+
842
+ ---
843
+
844
+ ## Summary Table
845
+
846
+ | Node Type | Condition Context | LLM Selection? | Jinja Support | Failure Signal |
847
+ |-----------|-------------------|----------------|---------------|----------------|
848
+ | Router | `context` | No | Yes | No |
849
+ | LLM | `context` | Yes (plain text) | Yes | `llm_failure_signal` |
850
+ | Agent | `context` | Yes (plain text) | Yes | `llm_failure_signal` |
851
+ | Tool | `result`, `context` | No | Yes | Registry-based |
852
+ | Child | `context` | No | Yes | No |
853
+
854
+ ---
855
+
856
+ ## Key Takeaways
857
+
858
+ 1. **No condition** = signal always emits
859
+ 2. **Jinja condition** = programmatic evaluation by SOE
860
+ 3. **Plain text condition** = semantic selection by LLM (LLM/Agent nodes only)
861
+ 4. **`result` keyword** = tool return value (Tool nodes only)
862
+ 5. **`signals_to_parent`** = controls sub-orchestration signal propagation
863
+ 6. **Failure signals** = error handling for LLM/Agent nodes
864
+ 7. Use **descriptive signal names** for maintainability
865
+ 8. **Jinja takes over**: If any condition has `{{ }}`, all are evaluated programmatically