soe-ai 0.1.1__py3-none-any.whl → 0.1.3__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.
- soe/builtin_tools/__init__.py +39 -0
- soe/builtin_tools/soe_add_signal.py +82 -0
- soe/builtin_tools/soe_call_tool.py +111 -0
- soe/builtin_tools/soe_copy_context.py +80 -0
- soe/builtin_tools/soe_explore_docs.py +290 -0
- soe/builtin_tools/soe_get_available_tools.py +42 -0
- soe/builtin_tools/soe_get_context.py +50 -0
- soe/builtin_tools/soe_get_workflows.py +63 -0
- soe/builtin_tools/soe_inject_node.py +86 -0
- soe/builtin_tools/soe_inject_workflow.py +105 -0
- soe/builtin_tools/soe_list_contexts.py +73 -0
- soe/builtin_tools/soe_remove_node.py +72 -0
- soe/builtin_tools/soe_remove_workflow.py +62 -0
- soe/builtin_tools/soe_update_context.py +54 -0
- soe/docs/_config.yml +10 -0
- soe/docs/advanced_patterns/guide_fanout_and_aggregations.md +318 -0
- soe/docs/advanced_patterns/guide_inheritance.md +435 -0
- soe/docs/advanced_patterns/hybrid_intelligence.md +237 -0
- soe/docs/advanced_patterns/index.md +49 -0
- soe/docs/advanced_patterns/operational.md +781 -0
- soe/docs/advanced_patterns/self_evolving_workflows.md +385 -0
- soe/docs/advanced_patterns/swarm_intelligence.md +211 -0
- soe/docs/builtins/context.md +164 -0
- soe/docs/builtins/explore_docs.md +135 -0
- soe/docs/builtins/tools.md +164 -0
- soe/docs/builtins/workflows.md +199 -0
- soe/docs/guide_00_getting_started.md +341 -0
- soe/docs/guide_01_tool.md +206 -0
- soe/docs/guide_02_llm.md +143 -0
- soe/docs/guide_03_router.md +146 -0
- soe/docs/guide_04_patterns.md +475 -0
- soe/docs/guide_05_agent.md +159 -0
- soe/docs/guide_06_schema.md +397 -0
- soe/docs/guide_07_identity.md +540 -0
- soe/docs/guide_08_child.md +612 -0
- soe/docs/guide_09_ecosystem.md +690 -0
- soe/docs/guide_10_infrastructure.md +427 -0
- soe/docs/guide_11_builtins.md +118 -0
- soe/docs/index.md +104 -0
- soe/docs/primitives/backends.md +281 -0
- soe/docs/primitives/context.md +256 -0
- soe/docs/primitives/node_reference.md +259 -0
- soe/docs/primitives/primitives.md +331 -0
- soe/docs/primitives/signals.md +865 -0
- soe/docs_index.py +1 -1
- soe/lib/__init__.py +0 -0
- soe/lib/child_context.py +46 -0
- soe/lib/context_fields.py +51 -0
- soe/lib/inheritance.py +172 -0
- soe/lib/jinja_render.py +113 -0
- soe/lib/operational.py +51 -0
- soe/lib/parent_sync.py +71 -0
- soe/lib/register_event.py +75 -0
- soe/lib/schema_validation.py +134 -0
- soe/lib/yaml_parser.py +14 -0
- soe/local_backends/__init__.py +18 -0
- soe/local_backends/factory.py +124 -0
- soe/local_backends/in_memory/context.py +38 -0
- soe/local_backends/in_memory/conversation_history.py +60 -0
- soe/local_backends/in_memory/identity.py +52 -0
- soe/local_backends/in_memory/schema.py +40 -0
- soe/local_backends/in_memory/telemetry.py +38 -0
- soe/local_backends/in_memory/workflow.py +33 -0
- soe/local_backends/storage/context.py +57 -0
- soe/local_backends/storage/conversation_history.py +82 -0
- soe/local_backends/storage/identity.py +118 -0
- soe/local_backends/storage/schema.py +96 -0
- soe/local_backends/storage/telemetry.py +72 -0
- soe/local_backends/storage/workflow.py +56 -0
- soe/nodes/__init__.py +13 -0
- soe/nodes/agent/__init__.py +10 -0
- soe/nodes/agent/factory.py +134 -0
- soe/nodes/agent/lib/loop_handlers.py +150 -0
- soe/nodes/agent/lib/loop_state.py +157 -0
- soe/nodes/agent/lib/prompts.py +65 -0
- soe/nodes/agent/lib/tools.py +35 -0
- soe/nodes/agent/stages/__init__.py +12 -0
- soe/nodes/agent/stages/parameter.py +37 -0
- soe/nodes/agent/stages/response.py +54 -0
- soe/nodes/agent/stages/router.py +37 -0
- soe/nodes/agent/state.py +111 -0
- soe/nodes/agent/types.py +66 -0
- soe/nodes/agent/validation/__init__.py +11 -0
- soe/nodes/agent/validation/config.py +95 -0
- soe/nodes/agent/validation/operational.py +24 -0
- soe/nodes/child/__init__.py +3 -0
- soe/nodes/child/factory.py +61 -0
- soe/nodes/child/state.py +59 -0
- soe/nodes/child/validation/__init__.py +11 -0
- soe/nodes/child/validation/config.py +126 -0
- soe/nodes/child/validation/operational.py +28 -0
- soe/nodes/lib/conditions.py +71 -0
- soe/nodes/lib/context.py +24 -0
- soe/nodes/lib/conversation_history.py +77 -0
- soe/nodes/lib/identity.py +64 -0
- soe/nodes/lib/llm_resolver.py +142 -0
- soe/nodes/lib/output.py +68 -0
- soe/nodes/lib/response_builder.py +91 -0
- soe/nodes/lib/signal_emission.py +79 -0
- soe/nodes/lib/signals.py +54 -0
- soe/nodes/lib/tools.py +100 -0
- soe/nodes/llm/__init__.py +7 -0
- soe/nodes/llm/factory.py +103 -0
- soe/nodes/llm/state.py +76 -0
- soe/nodes/llm/types.py +12 -0
- soe/nodes/llm/validation/__init__.py +11 -0
- soe/nodes/llm/validation/config.py +89 -0
- soe/nodes/llm/validation/operational.py +23 -0
- soe/nodes/router/__init__.py +3 -0
- soe/nodes/router/factory.py +37 -0
- soe/nodes/router/state.py +32 -0
- soe/nodes/router/validation/__init__.py +11 -0
- soe/nodes/router/validation/config.py +58 -0
- soe/nodes/router/validation/operational.py +16 -0
- soe/nodes/tool/factory.py +66 -0
- soe/nodes/tool/lib/__init__.py +11 -0
- soe/nodes/tool/lib/conditions.py +35 -0
- soe/nodes/tool/lib/failure.py +28 -0
- soe/nodes/tool/lib/parameters.py +67 -0
- soe/nodes/tool/state.py +66 -0
- soe/nodes/tool/types.py +27 -0
- soe/nodes/tool/validation/__init__.py +15 -0
- soe/nodes/tool/validation/config.py +132 -0
- soe/nodes/tool/validation/operational.py +16 -0
- soe/validation/__init__.py +18 -0
- soe/validation/config.py +195 -0
- soe/validation/jinja.py +54 -0
- soe/validation/operational.py +110 -0
- {soe_ai-0.1.1.dist-info → soe_ai-0.1.3.dist-info}/METADATA +5 -5
- soe_ai-0.1.3.dist-info/RECORD +137 -0
- {soe_ai-0.1.1.dist-info → soe_ai-0.1.3.dist-info}/WHEEL +1 -1
- soe_ai-0.1.1.dist-info/RECORD +0 -10
- {soe_ai-0.1.1.dist-info → soe_ai-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {soe_ai-0.1.1.dist-info → soe_ai-0.1.3.dist-info}/top_level.txt +0 -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: {{ context.text }}"
|
|
137
|
+
output_field: analysis
|
|
138
|
+
event_emissions:
|
|
139
|
+
- signal_name: HIGH_PRIORITY
|
|
140
|
+
condition: "{{ context.priority > 5 }}"
|
|
141
|
+
- signal_name: NORMAL_PRIORITY
|
|
142
|
+
condition: "{{ context.priority <= 5 }}"
|
|
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: {{ context.message }}"
|
|
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: "{{ context.data is defined and context.data }}"
|
|
241
|
+
- signal_name: NO_DATA
|
|
242
|
+
condition: "{{ context.data is not defined or not context.data }}"
|
|
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: {{ context.text }}"
|
|
277
|
+
output_field: analysis
|
|
278
|
+
event_emissions:
|
|
279
|
+
- signal_name: HIGH_PRIORITY
|
|
280
|
+
condition: "{{ context.priority > 5 }}"
|
|
281
|
+
- signal_name: NORMAL_PRIORITY
|
|
282
|
+
condition: "{{ context.priority <= 5 }}"
|
|
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: {{ context.message }}"
|
|
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: {{ context.request }}"
|
|
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: "{{ result.status == 'approved' }}"
|
|
355
|
+
- signal_name: PAYMENT_DECLINED
|
|
356
|
+
condition: "{{ result.status == 'declined' }}"
|
|
357
|
+
- signal_name: PAYMENT_PENDING
|
|
358
|
+
condition: "{{ result.status == 'pending' }}"
|
|
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: "{{ result.valid and context.customer.is_vip and context.order.total > 1000 }}"
|
|
401
|
+
- signal_name: VIP_ORDER
|
|
402
|
+
condition: "{{ result.valid and context.customer.is_vip }}"
|
|
403
|
+
- signal_name: LARGE_ORDER
|
|
404
|
+
condition: "{{ result.valid and context.order.total > 1000 }}"
|
|
405
|
+
- signal_name: STANDARD_ORDER
|
|
406
|
+
condition: "{{ result.valid }}"
|
|
407
|
+
- signal_name: INVALID_ORDER
|
|
408
|
+
condition: "{{ not result.valid }}"
|
|
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: {{ context.input }}"
|
|
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: {{ context.task }}"
|
|
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: {{ context.data_to_analyze }}"
|
|
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: "{{ context.type == 'a' }}"
|
|
667
|
+
- signal_name: TYPE_B
|
|
668
|
+
condition: "{{ context.type == 'b' }}"
|
|
669
|
+
- signal_name: TYPE_DEFAULT
|
|
670
|
+
condition: "{{ context.type not in ['a', 'b'] }}"
|
|
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: "{{ context.notify_user }}"
|
|
703
|
+
- signal_name: LOG_EVENT
|
|
704
|
+
condition: "{{ context.log_enabled }}"
|
|
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: "{{ context.order['line_items']|length > 0 and context.order.total > 0 }}"
|
|
741
|
+
- signal_name: ORDER_INVALID
|
|
742
|
+
condition: "{{ context.order['line_items']|length == 0 or context.order.total <= 0 }}"
|
|
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: "{{ result.charged == true }}"
|
|
754
|
+
- signal_name: PAYMENT_FAILED
|
|
755
|
+
condition: "{{ result.charged == false }}"
|
|
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 #{{ context.order.id }}"
|
|
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
|