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,281 @@
1
+
2
+ # Backends: Pluggable Storage
3
+
4
+ Backends are **dumb data retrievers**—they store and retrieve data, nothing more. All business logic lives in the orchestration engine, not in backends.
5
+
6
+ ---
7
+
8
+ ## The Principle: Dumb Data Retrievers
9
+
10
+ Backends follow a simple principle:
11
+
12
+ 1. **Store data** when told to
13
+ 2. **Retrieve data** when asked
14
+ 3. **No business logic** — no validation, no transformation, no side effects
15
+
16
+ This makes backends trivially swappable. Whether you use in-memory, local files, PostgreSQL, or DynamoDB, the orchestration engine works exactly the same.
17
+
18
+ ---
19
+
20
+ ## The 6 Backend Types
21
+
22
+ | Backend | Protocol | Required | Purpose |
23
+ |---------|----------|----------|---------|
24
+ | `context` | `ContextBackend` | ✅ Yes | Execution state dictionary |
25
+ | `workflow` | `WorkflowBackend` | ✅ Yes | Workflow definitions and current workflow name |
26
+ | `telemetry` | `TelemetryBackend` | ❌ Optional | Logs and events for debugging |
27
+ | `conversation_history` | `ConversationHistoryBackend` | ❌ Optional | LLM chat history per identity |
28
+ | `context_schema` | `ContextSchemaBackend` | ❌ Optional | Type validation for outputs |
29
+ | `identity` | `IdentityBackend` | ❌ Optional | System prompts for LLM identities |
30
+
31
+ ---
32
+
33
+ ## Quick Start: Built-in Backends
34
+
35
+ SOE provides two factory functions for common use cases:
36
+
37
+ ```python
38
+ from soe.local_backends import create_in_memory_backends, create_local_backends
39
+
40
+ # For testing: everything in memory (lost on restart)
41
+ backends = create_in_memory_backends()
42
+
43
+ # For development: persisted to local files
44
+ backends = create_local_backends(
45
+ context_storage_dir="./data/contexts",
46
+ workflow_storage_dir="./data/workflows",
47
+ telemetry_storage_dir="./data/telemetry",
48
+ conversation_history_storage_dir="./data/conversations",
49
+ context_schema_storage_dir="./data/schemas",
50
+ identity_storage_dir="./data/identities",
51
+ )
52
+ ```
53
+
54
+ ---
55
+
56
+ ## Implementing Custom Backends
57
+
58
+ Each backend is a Python Protocol (structural typing). Implement the required methods and you're done.
59
+
60
+ ### ContextBackend (Required)
61
+
62
+ Stores execution context—the shared state dictionary.
63
+
64
+ ```python
65
+ from typing import Dict, Any
66
+
67
+ class MyContextBackend:
68
+ """Store contexts in your database."""
69
+
70
+ def save_context(self, id: str, context: Dict[str, Any]) -> None:
71
+ """Save context for an execution ID."""
72
+ # Your database write here
73
+ pass
74
+
75
+ def get_context(self, id: str) -> Dict[str, Any]:
76
+ """Get context for an execution ID."""
77
+ # Your database read here
78
+ return {}
79
+ ```
80
+
81
+ ### WorkflowBackend (Required)
82
+
83
+ Stores workflow definitions and tracks current workflow.
84
+
85
+ ```python
86
+ from typing import Dict, Any
87
+
88
+ class MyWorkflowBackend:
89
+ """Store workflows in your database."""
90
+
91
+ def save_workflows_registry(self, id: str, workflows: Dict[str, Any]) -> None:
92
+ """Save all workflows for an execution."""
93
+ pass
94
+
95
+ def get_workflows_registry(self, id: str) -> Dict[str, Any]:
96
+ """Get all workflows for an execution."""
97
+ return {}
98
+
99
+ def save_current_workflow_name(self, id: str, name: str) -> None:
100
+ """Save current workflow name (for child transitions)."""
101
+ pass
102
+
103
+ def get_current_workflow_name(self, id: str) -> str:
104
+ """Get current workflow name."""
105
+ return ""
106
+ ```
107
+
108
+ ### TelemetryBackend (Optional)
109
+
110
+ Logs events for debugging and observability.
111
+
112
+ ```python
113
+ class MyTelemetryBackend:
114
+ """Log events to your observability stack."""
115
+
116
+ def log_event(self, execution_id: str, event_type: str, **event_data) -> None:
117
+ """Log an event. event_data contains timestamp and event-specific fields."""
118
+ pass
119
+ ```
120
+
121
+ ### ConversationHistoryBackend (Optional)
122
+
123
+ Persists LLM conversation history per identity.
124
+
125
+ ```python
126
+ from typing import List, Dict, Any
127
+
128
+ class MyConversationHistoryBackend:
129
+ """Store chat history in your database."""
130
+
131
+ def get_conversation_history(self, identity: str) -> List[Dict[str, Any]]:
132
+ """Get conversation history. Returns list of {role, content} dicts."""
133
+ return []
134
+
135
+ def append_to_conversation_history(self, identity: str, entry: Dict[str, Any]) -> None:
136
+ """Append a single message to history."""
137
+ pass
138
+
139
+ def save_conversation_history(self, identity: str, history: List[Dict[str, Any]]) -> None:
140
+ """Replace entire history."""
141
+ pass
142
+
143
+ def delete_conversation_history(self, identity: str) -> None:
144
+ """Delete all history for an identity."""
145
+ pass
146
+ ```
147
+
148
+ ### ContextSchemaBackend (Optional)
149
+
150
+ Stores output field type schemas for validation.
151
+
152
+ ```python
153
+ from typing import Dict, Any, Optional
154
+
155
+ class MyContextSchemaBackend:
156
+ """Store context schemas for output validation."""
157
+
158
+ def save_context_schema(self, execution_id: str, schema: Dict[str, Any]) -> None:
159
+ """Save context schema for an execution."""
160
+ pass
161
+
162
+ def get_context_schema(self, execution_id: str) -> Optional[Dict[str, Any]]:
163
+ """Get context schema. Returns None if not found."""
164
+ return None
165
+
166
+ def delete_context_schema(self, execution_id: str) -> bool:
167
+ """Delete context schema. Returns True if deleted."""
168
+ return False
169
+ ```
170
+
171
+ ### IdentityBackend (Optional)
172
+
173
+ Stores LLM identity system prompts.
174
+
175
+ ```python
176
+ from typing import Dict, Optional
177
+
178
+ class MyIdentityBackend:
179
+ """Store identity definitions."""
180
+
181
+ def save_identities(self, execution_id: str, identities: Dict[str, str]) -> None:
182
+ """Save identity definitions. Format: {identity_name: system_prompt}"""
183
+ pass
184
+
185
+ def get_identities(self, execution_id: str) -> Optional[Dict[str, str]]:
186
+ """Get all identities for an execution."""
187
+ return None
188
+
189
+ def get_identity(self, execution_id: str, identity_name: str) -> Optional[str]:
190
+ """Get a specific identity's system prompt."""
191
+ return None
192
+
193
+ def delete_identities(self, execution_id: str) -> bool:
194
+ """Delete all identities for an execution."""
195
+ return False
196
+ ```
197
+
198
+ ---
199
+
200
+ ## Combining Into a Backends Container
201
+
202
+ Pass your backends to orchestrate via a container object:
203
+
204
+ ```python
205
+ class MyBackends:
206
+ def __init__(self):
207
+ self.context = MyContextBackend()
208
+ self.workflow = MyWorkflowBackend()
209
+ self.telemetry = MyTelemetryBackend() # or None
210
+ self.conversation_history = MyConversationHistoryBackend() # or None
211
+ self.context_schema = MyContextSchemaBackend() # or None
212
+ self.identity = MyIdentityBackend() # or None
213
+
214
+ backends = MyBackends()
215
+ ```
216
+
217
+ ---
218
+
219
+ ## Database Recommendations
220
+
221
+ ### Single Database, Multiple Tables
222
+
223
+ We recommend using **one database** with **separate tables** for each backend:
224
+
225
+ ```
226
+ your_database/
227
+ ├── contexts # ContextBackend
228
+ ├── workflows # WorkflowBackend
229
+ ├── telemetry # TelemetryBackend
230
+ ├── conversations # ConversationHistoryBackend
231
+ ├── context_schemas # ContextSchemaBackend
232
+ └── identities # IdentityBackend
233
+ ```
234
+
235
+ This gives you:
236
+ - **Transactional consistency** across backends
237
+ - **Simpler deployment** (one connection string)
238
+ - **Easier backups** (one database to backup)
239
+
240
+ ### Example: PostgreSQL Tables
241
+
242
+ ```sql
243
+ CREATE TABLE contexts (
244
+ execution_id VARCHAR PRIMARY KEY,
245
+ context JSONB NOT NULL,
246
+ created_at TIMESTAMP DEFAULT NOW()
247
+ );
248
+
249
+ CREATE TABLE workflows (
250
+ execution_id VARCHAR PRIMARY KEY,
251
+ registry JSONB NOT NULL,
252
+ current_workflow VARCHAR NOT NULL
253
+ );
254
+
255
+ CREATE TABLE telemetry (
256
+ id SERIAL PRIMARY KEY,
257
+ execution_id VARCHAR NOT NULL,
258
+ event_type VARCHAR NOT NULL,
259
+ event_data JSONB,
260
+ timestamp TIMESTAMP DEFAULT NOW()
261
+ );
262
+ ```
263
+
264
+ ---
265
+
266
+ ## Key Insight: Execution ID as Primary Key
267
+
268
+ Most backends key data by `execution_id`:
269
+
270
+ - **Context**: One context dict per execution
271
+ - **Workflows**: Workflow registry per execution
272
+ - **Telemetry**: Events indexed by execution
273
+
274
+ For **conversation history**, **context schema**, and **identities**, data is keyed by `main_execution_id`—this allows child workflows to access parent's definitions.
275
+
276
+ ---
277
+
278
+ ## See Also
279
+
280
+ - [Chapter 10: Infrastructure](../guide_10_infrastructure.md) — Full implementation examples
281
+ - [Primitives Overview](primitives.md) — All 7 SOE primitives
@@ -0,0 +1,256 @@
1
+
2
+ # Context: State Management with History
3
+
4
+ Context is the shared state dictionary for an execution. Unlike simple key-value stores, SOE context maintains **history** for each field—every update is appended, not replaced.
5
+
6
+ ---
7
+
8
+ ## Context as History
9
+
10
+ Each context field is stored as a **list** of values:
11
+
12
+ ```python
13
+ # After three tool calls that update "result":
14
+ context = {
15
+ "result": ["first", "second", "third"], # History preserved
16
+ "user_id": ["alice"], # Single value, still a list
17
+ "__operational__": {...} # Internal fields not wrapped
18
+ }
19
+ ```
20
+
21
+ When you read a field:
22
+ - **Reading normally** returns the **latest** value (last item)
23
+ - **Reading with `| accumulated`** returns the **full history** (all items)
24
+
25
+ ---
26
+
27
+ ## Reading Context in Jinja
28
+
29
+ ### Get Latest Value (Default)
30
+
31
+
32
+ ```yaml
33
+ prompt: |
34
+ User {{ context.user_id }} asked: {{ context.user_request }}
35
+
36
+ Previous result: {{ context.result }}
37
+ ```
38
+
39
+
40
+ This returns the **most recent** value for each field.
41
+
42
+ ### Get Full History with `| accumulated`
43
+
44
+
45
+ ```yaml
46
+ prompt: |
47
+ All results so far:
48
+ {% for r in context.result | accumulated %}
49
+ - {{ r }}
50
+ {% endfor %}
51
+
52
+ condition: "{{ (context.result | accumulated) | length >= 3 }}"
53
+ ```
54
+
55
+
56
+ Use `| accumulated` when you need:
57
+ - All previous LLM responses
58
+ - Aggregating results from multiple tool calls
59
+ - Checking how many updates have occurred
60
+
61
+ ---
62
+
63
+ ## Context in Conditions
64
+
65
+ Context is available in all condition expressions:
66
+
67
+
68
+ ```yaml
69
+ event_emissions:
70
+ # Check latest value
71
+ - signal_name: HAS_RESULT
72
+ condition: "{{ context.result is defined }}"
73
+
74
+ # Check history length
75
+ - signal_name: ENOUGH_DATA
76
+ condition: "{{ (context.data | accumulated) | length >= 5 }}"
77
+
78
+ # Complex conditions
79
+ - signal_name: VALIDATED
80
+ condition: "{{ context.user_id and context.user_profile.verified }}"
81
+ ```
82
+
83
+
84
+ ---
85
+
86
+ ## Context in Prompts
87
+
88
+ Full context is available in LLM/Agent prompts:
89
+
90
+
91
+ ```yaml
92
+ prompt: |
93
+ You are helping user {{ context.user_id }}.
94
+
95
+ Their profile: {{ context.user_profile | tojson }}
96
+
97
+ Conversation so far:
98
+ {% for msg in context.messages | accumulated %}
99
+ {{ msg.role }}: {{ msg.content }}
100
+ {% endfor %}
101
+
102
+ Current request: {{ context.user_request }}
103
+ ```
104
+
105
+
106
+ ---
107
+
108
+ ## How Context is Updated
109
+
110
+ ### Tool Nodes
111
+
112
+ Tool output is saved via `output_field`:
113
+
114
+ ```yaml
115
+ ProcessData:
116
+ node_type: tool
117
+ tool_name: process
118
+ input_fields: [raw_data]
119
+ output_field: processed # Tool return value saved here
120
+ ```
121
+
122
+ Each execution **appends** to the field's history.
123
+
124
+ ### LLM/Agent Nodes
125
+
126
+ LLM response is saved via `output_field`:
127
+
128
+
129
+ ```yaml
130
+ Summarize:
131
+ node_type: llm
132
+ prompt: "Summarize: {{ context.data }}"
133
+ output_field: summary # LLM response saved here
134
+ ```
135
+
136
+
137
+ ### Initial Context
138
+
139
+ When starting orchestration:
140
+
141
+ ```python
142
+ orchestrate(
143
+ config=workflow,
144
+ initial_workflow_name="my_workflow",
145
+ initial_signals=["START"],
146
+ initial_context={
147
+ "user_id": "alice",
148
+ "user_request": "Help me with X"
149
+ },
150
+ backends=backends,
151
+ broadcast_signals_caller=broadcast,
152
+ )
153
+ ```
154
+
155
+ Initial context fields are wrapped in lists automatically:
156
+ ```python
157
+ # Internal representation:
158
+ {"user_id": ["alice"], "user_request": ["Help me with X"]}
159
+ ```
160
+
161
+ ---
162
+
163
+ ## The `__operational__` Field
164
+
165
+ Context includes a special `__operational__` field that tracks execution metadata:
166
+
167
+ ```python
168
+ context["__operational__"] = {
169
+ "signals": ["START", "VALIDATED"], # Signals emitted
170
+ "nodes": {"ValidateInput": 1}, # Node execution counts
171
+ "llm_calls": 3, # Total LLM calls
172
+ "tool_calls": 5, # Total tool calls
173
+ "errors": 0, # Error count
174
+ "main_execution_id": "abc-123" # Root execution ID
175
+ }
176
+ ```
177
+
178
+ **Important**: Fields starting with `__` are **not** wrapped in lists—they store direct values.
179
+
180
+ ---
181
+
182
+ ## Practical Examples
183
+
184
+ ### Aggregating Multiple Results
185
+
186
+
187
+ ```yaml
188
+ # Fan-out pattern: multiple child workflows write to same field
189
+ AggregateResults:
190
+ node_type: router
191
+ event_triggers: [CHILD_COMPLETE]
192
+ event_emissions:
193
+ - signal_name: ALL_DONE
194
+ condition: "{{ (context.child_results | accumulated) | length >= 3 }}"
195
+ ```
196
+
197
+
198
+ ### Retry with History
199
+
200
+
201
+ ```yaml
202
+ RetryLogic:
203
+ node_type: router
204
+ event_triggers: [FAILED]
205
+ event_emissions:
206
+ - signal_name: RETRY
207
+ condition: "{{ (context.attempts | accumulated) | length < 3 }}"
208
+ - signal_name: GIVE_UP
209
+ condition: "{{ (context.attempts | accumulated) | length >= 3 }}"
210
+ ```
211
+
212
+
213
+ ### Building Context Over Time
214
+
215
+
216
+ ```yaml
217
+ # Each step adds to enrichment
218
+ EnrichStep1:
219
+ node_type: tool
220
+ tool_name: get_user_profile
221
+ input_fields: [user_id]
222
+ output_field: enrichment
223
+
224
+ EnrichStep2:
225
+ node_type: tool
226
+ tool_name: get_purchase_history
227
+ input_fields: [user_id]
228
+ output_field: enrichment # Same field, appends
229
+
230
+ # Later: context.enrichment | accumulated = [profile, history]
231
+ ```
232
+
233
+
234
+ ---
235
+
236
+ ## Context Backend
237
+
238
+ Context is persisted via `ContextBackend`:
239
+
240
+ ```python
241
+ # Save
242
+ backends.context.save_context(execution_id, context)
243
+
244
+ # Retrieve
245
+ context = backends.context.get_context(execution_id)
246
+ ```
247
+
248
+ See [Backends](backends.md) for implementation details.
249
+
250
+ ---
251
+
252
+ ## See Also
253
+
254
+ - [Backends](backends.md) — How context is persisted
255
+ - [Signals](signals.md) — How signals trigger nodes based on context
256
+ - [Primitives Overview](primitives.md) — All 7 SOE primitives