soe-ai 0.1.1__py3-none-any.whl → 0.1.2__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 (134) hide show
  1. soe/builtin_tools/__init__.py +39 -0
  2. soe/builtin_tools/soe_add_signal.py +82 -0
  3. soe/builtin_tools/soe_call_tool.py +111 -0
  4. soe/builtin_tools/soe_copy_context.py +80 -0
  5. soe/builtin_tools/soe_explore_docs.py +290 -0
  6. soe/builtin_tools/soe_get_available_tools.py +42 -0
  7. soe/builtin_tools/soe_get_context.py +50 -0
  8. soe/builtin_tools/soe_get_workflows.py +63 -0
  9. soe/builtin_tools/soe_inject_node.py +86 -0
  10. soe/builtin_tools/soe_inject_workflow.py +105 -0
  11. soe/builtin_tools/soe_list_contexts.py +73 -0
  12. soe/builtin_tools/soe_remove_node.py +72 -0
  13. soe/builtin_tools/soe_remove_workflow.py +62 -0
  14. soe/builtin_tools/soe_update_context.py +54 -0
  15. soe/docs/_config.yml +10 -0
  16. soe/docs/advanced_patterns/guide_fanout_and_aggregations.md +318 -0
  17. soe/docs/advanced_patterns/guide_inheritance.md +435 -0
  18. soe/docs/advanced_patterns/hybrid_intelligence.md +237 -0
  19. soe/docs/advanced_patterns/index.md +49 -0
  20. soe/docs/advanced_patterns/operational.md +781 -0
  21. soe/docs/advanced_patterns/self_evolving_workflows.md +385 -0
  22. soe/docs/advanced_patterns/swarm_intelligence.md +211 -0
  23. soe/docs/builtins/context.md +164 -0
  24. soe/docs/builtins/explore_docs.md +135 -0
  25. soe/docs/builtins/tools.md +164 -0
  26. soe/docs/builtins/workflows.md +199 -0
  27. soe/docs/guide_00_getting_started.md +341 -0
  28. soe/docs/guide_01_tool.md +206 -0
  29. soe/docs/guide_02_llm.md +143 -0
  30. soe/docs/guide_03_router.md +146 -0
  31. soe/docs/guide_04_patterns.md +475 -0
  32. soe/docs/guide_05_agent.md +159 -0
  33. soe/docs/guide_06_schema.md +397 -0
  34. soe/docs/guide_07_identity.md +540 -0
  35. soe/docs/guide_08_child.md +612 -0
  36. soe/docs/guide_09_ecosystem.md +690 -0
  37. soe/docs/guide_10_infrastructure.md +427 -0
  38. soe/docs/guide_11_builtins.md +118 -0
  39. soe/docs/index.md +104 -0
  40. soe/docs/primitives/backends.md +281 -0
  41. soe/docs/primitives/context.md +256 -0
  42. soe/docs/primitives/node_reference.md +259 -0
  43. soe/docs/primitives/primitives.md +331 -0
  44. soe/docs/primitives/signals.md +865 -0
  45. soe/docs_index.py +1 -1
  46. soe/lib/__init__.py +0 -0
  47. soe/lib/child_context.py +46 -0
  48. soe/lib/context_fields.py +51 -0
  49. soe/lib/inheritance.py +172 -0
  50. soe/lib/jinja_render.py +113 -0
  51. soe/lib/operational.py +51 -0
  52. soe/lib/parent_sync.py +71 -0
  53. soe/lib/register_event.py +75 -0
  54. soe/lib/schema_validation.py +134 -0
  55. soe/lib/yaml_parser.py +14 -0
  56. soe/local_backends/__init__.py +18 -0
  57. soe/local_backends/factory.py +124 -0
  58. soe/local_backends/in_memory/context.py +38 -0
  59. soe/local_backends/in_memory/conversation_history.py +60 -0
  60. soe/local_backends/in_memory/identity.py +52 -0
  61. soe/local_backends/in_memory/schema.py +40 -0
  62. soe/local_backends/in_memory/telemetry.py +38 -0
  63. soe/local_backends/in_memory/workflow.py +33 -0
  64. soe/local_backends/storage/context.py +57 -0
  65. soe/local_backends/storage/conversation_history.py +82 -0
  66. soe/local_backends/storage/identity.py +118 -0
  67. soe/local_backends/storage/schema.py +96 -0
  68. soe/local_backends/storage/telemetry.py +72 -0
  69. soe/local_backends/storage/workflow.py +56 -0
  70. soe/nodes/__init__.py +13 -0
  71. soe/nodes/agent/__init__.py +10 -0
  72. soe/nodes/agent/factory.py +134 -0
  73. soe/nodes/agent/lib/loop_handlers.py +150 -0
  74. soe/nodes/agent/lib/loop_state.py +157 -0
  75. soe/nodes/agent/lib/prompts.py +65 -0
  76. soe/nodes/agent/lib/tools.py +35 -0
  77. soe/nodes/agent/stages/__init__.py +12 -0
  78. soe/nodes/agent/stages/parameter.py +37 -0
  79. soe/nodes/agent/stages/response.py +54 -0
  80. soe/nodes/agent/stages/router.py +37 -0
  81. soe/nodes/agent/state.py +111 -0
  82. soe/nodes/agent/types.py +66 -0
  83. soe/nodes/agent/validation/__init__.py +11 -0
  84. soe/nodes/agent/validation/config.py +95 -0
  85. soe/nodes/agent/validation/operational.py +24 -0
  86. soe/nodes/child/__init__.py +3 -0
  87. soe/nodes/child/factory.py +61 -0
  88. soe/nodes/child/state.py +59 -0
  89. soe/nodes/child/validation/__init__.py +11 -0
  90. soe/nodes/child/validation/config.py +126 -0
  91. soe/nodes/child/validation/operational.py +28 -0
  92. soe/nodes/lib/conditions.py +71 -0
  93. soe/nodes/lib/context.py +24 -0
  94. soe/nodes/lib/conversation_history.py +77 -0
  95. soe/nodes/lib/identity.py +64 -0
  96. soe/nodes/lib/llm_resolver.py +142 -0
  97. soe/nodes/lib/output.py +68 -0
  98. soe/nodes/lib/response_builder.py +91 -0
  99. soe/nodes/lib/signal_emission.py +79 -0
  100. soe/nodes/lib/signals.py +54 -0
  101. soe/nodes/lib/tools.py +100 -0
  102. soe/nodes/llm/__init__.py +7 -0
  103. soe/nodes/llm/factory.py +103 -0
  104. soe/nodes/llm/state.py +76 -0
  105. soe/nodes/llm/types.py +12 -0
  106. soe/nodes/llm/validation/__init__.py +11 -0
  107. soe/nodes/llm/validation/config.py +89 -0
  108. soe/nodes/llm/validation/operational.py +23 -0
  109. soe/nodes/router/__init__.py +3 -0
  110. soe/nodes/router/factory.py +37 -0
  111. soe/nodes/router/state.py +32 -0
  112. soe/nodes/router/validation/__init__.py +11 -0
  113. soe/nodes/router/validation/config.py +58 -0
  114. soe/nodes/router/validation/operational.py +16 -0
  115. soe/nodes/tool/factory.py +66 -0
  116. soe/nodes/tool/lib/__init__.py +11 -0
  117. soe/nodes/tool/lib/conditions.py +35 -0
  118. soe/nodes/tool/lib/failure.py +28 -0
  119. soe/nodes/tool/lib/parameters.py +67 -0
  120. soe/nodes/tool/state.py +66 -0
  121. soe/nodes/tool/types.py +27 -0
  122. soe/nodes/tool/validation/__init__.py +15 -0
  123. soe/nodes/tool/validation/config.py +132 -0
  124. soe/nodes/tool/validation/operational.py +16 -0
  125. soe/validation/__init__.py +18 -0
  126. soe/validation/config.py +195 -0
  127. soe/validation/jinja.py +54 -0
  128. soe/validation/operational.py +110 -0
  129. {soe_ai-0.1.1.dist-info → soe_ai-0.1.2.dist-info}/METADATA +4 -4
  130. soe_ai-0.1.2.dist-info/RECORD +137 -0
  131. {soe_ai-0.1.1.dist-info → soe_ai-0.1.2.dist-info}/WHEEL +1 -1
  132. soe_ai-0.1.1.dist-info/RECORD +0 -10
  133. {soe_ai-0.1.1.dist-info → soe_ai-0.1.2.dist-info}/licenses/LICENSE +0 -0
  134. {soe_ai-0.1.1.dist-info → soe_ai-0.1.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,159 @@
1
+
2
+ # SOE Guide: Chapter 5 - Agent Nodes
3
+
4
+ ## Introduction to Agent Nodes
5
+
6
+ The **Agent Node** is a convenience wrapper that encapsulates the **ReAct pattern** (Reasoning + Acting) into a single, configurable component.
7
+
8
+ In the previous chapter, you learned how to build custom agent loops using Tool, LLM, and Router nodes. The Agent Node does the same thing—but packaged for common use cases where you want a "hands-off" tool-using agent.
9
+
10
+ ### Why Use Agent Nodes?
11
+
12
+ | Building Custom (Ch. 4) | Using Agent Node |
13
+ |------------------------|------------------|
14
+ | Full control over each step | Quick to configure |
15
+ | Custom reasoning patterns | Standard ReAct loop |
16
+ | Explicit debugging points | Batteries included |
17
+ | Any architecture you want | Opinionated but effective |
18
+
19
+ **Use Agent Node when**: You need a straightforward tool-using agent and don't need custom reasoning patterns.
20
+
21
+ **Use custom workflows when**: You need chain-of-thought, metacognition, voting, or other advanced patterns.
22
+
23
+ **Note**: Agent nodes can also be used for other patterns (routing, summarization, etc.), but they involve more LLM calls than specialized nodes. Use the right node type for your use case.
24
+
25
+ ### The Agent Loop
26
+
27
+ The Agent Node runs this loop internally:
28
+
29
+ 1. **Router Stage**: The agent decides what to do (Call a Tool or Finish)
30
+ 2. **Parameter Stage**: If calling a tool, it generates the arguments
31
+ 3. **Execution Stage**: The tool is executed
32
+ 4. **Loop**: The results are fed back into the history, and the agent decides again
33
+
34
+ This is exactly what you built manually in Chapter 4's "Custom ReAct Loop" pattern—but as a single node.
35
+
36
+ ### Multiple Tool Calls
37
+
38
+ The agent can call multiple tools in sequence during a single node execution:
39
+
40
+ 1. Agent decides to call Tool A → executes → sees result
41
+ 2. Agent decides to call Tool B → executes → sees result
42
+ 3. Agent decides to call Tool A again → executes → sees result
43
+ 4. Agent decides to Finish
44
+
45
+ **Exhaustive tool calling**: The agent will keep calling tools until it decides to finish. There's no hard limit on tool calls—the agent uses its judgment. To prevent runaway loops, use operational limits (see [Operational Features](advanced_patterns/operational.md)).
46
+
47
+ ## Configuring Tools
48
+
49
+ Agents have access to the tools you provide in their `tools` list.
50
+
51
+ > [!IMPORTANT]
52
+ > **Tool Restriction**: Agent nodes can ONLY call tools explicitly listed in their `tools` configuration, even if a tool is a SOE built-in. If you want an agent to be able to use a built-in tool like `soe_inject_node`, you must include it in the `tools` list for that node.
53
+
54
+ ### Example: Tool Restriction
55
+
56
+ ```yaml
57
+ MyAgent:
58
+ node_type: agent
59
+ tools:
60
+ - calculate_total # User tool
61
+ - soe_inject_node # Built-in tool (MUST be listed here)
62
+ ```
63
+
64
+ ### The Workflow
65
+
66
+ ```yaml
67
+ example_workflow:
68
+ CalculatorAgent:
69
+ node_type: agent
70
+ event_triggers: [START]
71
+ prompt: "You are a calculator. Solve the user's math problem: {{ context.problem }}"
72
+ tools: [calculator]
73
+ output_field: result
74
+ event_emissions:
75
+ - signal_name: CALCULATION_DONE
76
+ ```
77
+
78
+ ### How It Works
79
+
80
+ 1. **`tools`**: We give the agent a list of tools (e.g., `[calculator]`).
81
+ 2. **The Loop**:
82
+ * The agent sees the prompt: "Solve... {{ context.problem }}"
83
+ * It decides to call `calculator(5, 3, 'add')`.
84
+ * The tool returns `8`.
85
+ * The agent sees the result and decides to **Finish**.
86
+ 3. **Output**: The final answer is stored in `context.result`.
87
+
88
+ ## Advanced Configuration: Multiple Tools
89
+
90
+ Agents can handle complex tasks with multiple tools.
91
+
92
+ ### The Workflow
93
+
94
+ ```yaml
95
+ example_workflow:
96
+ ResearchAgent:
97
+ node_type: agent
98
+ event_triggers: [START]
99
+ prompt: "Research the topic: {{ context.topic }}"
100
+ tools: [search_web, summarize_text]
101
+ output_field: report
102
+ event_emissions:
103
+ - signal_name: REPORT_READY
104
+ ```
105
+
106
+ ### Robustness Features
107
+
108
+ - **Tool Selection**: The agent intelligently chooses between `search_web` and `summarize_text` based on the goal.
109
+ - **Error Recovery**: If a tool fails (throws an exception), the agent sees the error and can try again or try a different strategy.
110
+
111
+ ## Agent Signal Selection
112
+
113
+ Like LLM nodes, Agent nodes can select which signal to emit based on their analysis:
114
+
115
+ ### The Workflow
116
+
117
+ ```yaml
118
+ example_workflow:
119
+ AnalysisAgent:
120
+ node_type: agent
121
+ event_triggers: [START]
122
+ prompt: "Analyze the data and determine if it needs human review: {{ context.data }}"
123
+ tools: [analyze_data]
124
+ output_field: analysis
125
+ event_emissions:
126
+ - signal_name: AUTO_APPROVE
127
+ condition: "The analysis shows the data is clearly valid"
128
+ - signal_name: NEEDS_REVIEW
129
+ condition: "The analysis shows the data requires human review"
130
+ - signal_name: REJECT
131
+ condition: "The analysis shows the data is clearly invalid"
132
+ ```
133
+
134
+ The agent completes its task, then the signal selection works the same as LLM nodes:
135
+ - **Plain text conditions**: LLM chooses semantically
136
+ - **Jinja conditions**: Evaluated programmatically
137
+
138
+ ## When to Choose Agent vs Custom Workflow
139
+
140
+ ### Use Agent Node For:
141
+
142
+ - **Quick prototyping**: Get a tool-using agent running fast
143
+ - **Standard tasks**: Research, calculation, data gathering
144
+ - **Simple tool orchestration**: When tool selection is the main complexity
145
+
146
+ ### Build Custom Workflows For:
147
+
148
+ - **Chain-of-thought reasoning**: Explicit step-by-step thinking
149
+ - **Metacognition**: Self-review and refinement loops
150
+ - **Parallel/voting patterns**: Multiple analyses combined
151
+ - **Hybrid logic**: Mixing deterministic gates with AI
152
+ - **Audit requirements**: When you need to log/inspect each decision
153
+ - **Custom termination conditions**: Beyond simple "finish" detection
154
+
155
+ Remember: The Agent Node is **syntactic sugar** for a common pattern. Everything it does, you can build yourself with the three core node types.
156
+
157
+ ## Next Steps
158
+
159
+ Now that you understand both custom workflows and the Agent Node, let's explore [Schemas](guide_06_schema.md) for structured LLM output →
@@ -0,0 +1,397 @@
1
+
2
+ # SOE Guide: Chapter 6 - Context Schema
3
+
4
+ ## Introduction to Context Schema
5
+
6
+ **Context Schema** provides optional type validation for context fields. When an LLM node writes to a context field, the schema ensures the output matches the expected type (string, integer, object, etc.).
7
+
8
+ > **Note**: This was previously called just "Schema". We renamed it to "Context Schema" to distinguish it from the Identity Schema (see [Chapter 7](guide_07_identity.md)).
9
+
10
+ ### Why Use Context Schema?
11
+
12
+ - **Type Safety**: Catch malformed LLM output before it breaks downstream nodes.
13
+ - **Tool Integration**: Ensure LLM output has the correct structure for tools.
14
+ - **Documentation**: Schema definitions serve as documentation for your workflow's data model.
15
+ - **Removes Prompt Boilerplate**: You don't need to specify output format in every prompt—the schema handles it.
16
+
17
+ ## Defining a Schema
18
+
19
+ Schemas are defined per-workflow, mapping field names to their types:
20
+
21
+ ```python
22
+ schemas = {
23
+ "example_workflow": {
24
+ "summary": {
25
+ "type": "string",
26
+ "description": "A one-sentence summary of the input text"
27
+ }
28
+ }
29
+ }
30
+ ```
31
+
32
+ ### Available Types
33
+
34
+ | Type | Python Type | Description |
35
+ |------|-------------|-------------|
36
+ | `string` | `str` | Text values |
37
+ | `integer` | `int` | Whole numbers |
38
+ | `number` | `float` | Decimal numbers |
39
+ | `boolean` | `bool` | True/False |
40
+ | `object` | `dict` | JSON objects |
41
+ | `list` | `list` | Arrays |
42
+ | `dict` | `dict` | Alias for object |
43
+
44
+ ## Your First Schema (Full Config)
45
+
46
+ Let's validate that an LLM returns a proper string summary using the **combined config** format (workflows + context_schema in one YAML).
47
+
48
+ ### Full Workflow + Schema (Config)
49
+
50
+ ```yaml
51
+ workflows:
52
+ example_workflow:
53
+ SummarizeLLM:
54
+ node_type: llm
55
+ event_triggers: [START]
56
+ prompt: "Summarize the following text in one sentence: {{ context.input_text }}"
57
+ output_field: summary
58
+ event_emissions:
59
+ - signal_name: SUMMARY_COMPLETE
60
+
61
+ context_schema:
62
+ summary:
63
+ type: string
64
+ description: A one-sentence summary of the input text
65
+ ```
66
+
67
+ ### How It Works
68
+
69
+ 1. The LLM node writes to `output_field: summary`.
70
+ 2. Schema backend finds the schema for `summary`.
71
+ 3. The LLM returns the **schema value directly** (no wrapper key).
72
+ 4. Valid output → saved to context under `summary` → `SUMMARY_COMPLETE` emitted.
73
+
74
+ ## Integer Schema (Full Config)
75
+
76
+ For numeric outputs like counts or scores:
77
+
78
+ ### Full Workflow + Schema (Config)
79
+
80
+ ```yaml
81
+ workflows:
82
+ example_workflow:
83
+ CounterLLM:
84
+ node_type: llm
85
+ event_triggers: [START]
86
+ prompt: "Count the number of words in this text: {{ context.input_text }}. Return only the count."
87
+ output_field: word_count
88
+ event_emissions:
89
+ - signal_name: COUNT_COMPLETE
90
+
91
+ context_schema:
92
+ word_count:
93
+ type: integer
94
+ description: The number of words in the input text
95
+ ```
96
+
97
+ The LLM must return `42` (an integer), not `"forty-two"`.
98
+
99
+ ## Object Schema (Full Config)
100
+
101
+ For structured data extraction:
102
+
103
+ ### Full Workflow + Schema (Config)
104
+
105
+ ```yaml
106
+ workflows:
107
+ example_workflow:
108
+ ExtractorLLM:
109
+ node_type: llm
110
+ event_triggers: [START]
111
+ prompt: "Extract the person's name and age from: {{ context.input_text }}. Return as JSON with 'name' and 'age' fields."
112
+ output_field: person_data
113
+ event_emissions:
114
+ - signal_name: EXTRACTION_COMPLETE
115
+
116
+ context_schema:
117
+ person_data:
118
+ type: object
119
+ description: Extracted person data with name and age
120
+ properties:
121
+ name:
122
+ type: string
123
+ age:
124
+ type: integer
125
+ ```
126
+
127
+ Object schemas accept JSON objects. You can also define nested fields with `properties`.
128
+
129
+ ### Nested Object Schema (with `properties`)
130
+
131
+ ```yaml
132
+ context_schema:
133
+ person_data:
134
+ type: object
135
+ description: Person data
136
+ properties:
137
+ name:
138
+ type: string
139
+ age:
140
+ type: integer
141
+ address:
142
+ type: object
143
+ properties:
144
+ city:
145
+ type: string
146
+ zip:
147
+ type: string
148
+ ```
149
+
150
+ **Valid LLM output (no wrapper):**
151
+
152
+ ```json
153
+ {"name": "Bob", "age": 25, "address": {"city": "NYC", "zip": "10001"}}
154
+ ```
155
+
156
+ ## Schema with Tool Integration (Full Config)
157
+
158
+ Schema shines when LLM output feeds into tool parameters. This ensures the LLM returns data in the exact format your tool expects.
159
+
160
+ ### Full Workflow + Schema (Config)
161
+
162
+ ```yaml
163
+ workflows:
164
+ example_workflow:
165
+ ParameterExtractor:
166
+ node_type: llm
167
+ event_triggers: [START]
168
+ prompt: "Extract the operation and numbers from: {{ context.user_request }}. Return JSON with 'operation' (add/multiply) and 'numbers' (list of integers)."
169
+ output_field: params
170
+ event_emissions:
171
+ - signal_name: PARAMS_EXTRACTED
172
+
173
+ Calculator:
174
+ node_type: tool
175
+ event_triggers: [PARAMS_EXTRACTED]
176
+ tool_name: calculate
177
+ context_parameter_field: params
178
+ output_field: result
179
+ event_emissions:
180
+ - signal_name: CALCULATED
181
+
182
+ context_schema:
183
+ params:
184
+ type: object
185
+ description: Extracted parameters with operation and numbers
186
+ properties:
187
+ operation:
188
+ type: string
189
+ numbers:
190
+ type: list
191
+ items:
192
+ type: integer
193
+ result:
194
+ type: object
195
+ description: Calculation result
196
+ ```
197
+
198
+ ### Data Flow
199
+
200
+ 1. `ParameterExtractor` LLM extracts `{ "operation": "add", "numbers": [10, 20, 30] }`.
201
+ 2. Schema validates this is an object (dict).
202
+ 3. `Calculator` tool receives the validated params.
203
+ 4. Tool returns result, also validated against schema.
204
+
205
+ ## Multiple Fields (Full Config)
206
+
207
+ A single workflow can have schemas for multiple fields:
208
+
209
+ ### Full Workflow + Schema (Config)
210
+
211
+ ```yaml
212
+ workflows:
213
+ example_workflow:
214
+ AnalyzerLLM:
215
+ node_type: llm
216
+ event_triggers: [START]
217
+ prompt: "Analyze this text: {{ context.input_text }}. Extract the topic and key points."
218
+ output_field: topic
219
+ event_emissions:
220
+ - signal_name: TOPIC_EXTRACTED
221
+
222
+ SummarizerLLM:
223
+ node_type: llm
224
+ event_triggers: [TOPIC_EXTRACTED]
225
+ prompt: "Given the topic '{{ context.topic }}', provide a brief summary of: {{ context.input_text }}"
226
+ output_field: summary
227
+ event_emissions:
228
+ - signal_name: ANALYSIS_COMPLETE
229
+
230
+ context_schema:
231
+ topic:
232
+ type: string
233
+ description: The main topic of the text
234
+ summary:
235
+ type: string
236
+ description: A brief summary based on the topic
237
+ ```
238
+
239
+ Each field is validated independently when its LLM node completes.
240
+
241
+ ## Agent Node + Schema (Full Config)
242
+
243
+ ```yaml
244
+ workflows:
245
+ example_workflow:
246
+ DataAgent:
247
+ node_type: agent
248
+ event_triggers: [START]
249
+ prompt: "Process this request: {{ context.user_request }}"
250
+ tools: [fetch_data]
251
+ output_field: response
252
+ event_emissions:
253
+ - signal_name: AGENT_COMPLETE
254
+
255
+ context_schema:
256
+ response:
257
+ type: string
258
+ description: The agent's final response to the user
259
+ ```
260
+
261
+ The agent response is validated against the schema for `response`.
262
+
263
+ ## Schema is Optional (Workflow Only)
264
+
265
+ Schemas are completely optional. Workflows work fine without them:
266
+
267
+ ### The Workflow (No context_schema)
268
+
269
+ ```yaml
270
+ example_workflow:
271
+ FreeLLM:
272
+ node_type: llm
273
+ event_triggers: [START]
274
+ prompt: "Do whatever you want with: {{ context.input_text }}"
275
+ output_field: output
276
+ event_emissions:
277
+ - signal_name: DONE
278
+ ```
279
+
280
+ Without schema, LLM output is saved as-is without validation. This is fine for:
281
+ - Prototyping
282
+ - Free-form text generation
283
+ - When you trust the LLM output format
284
+
285
+ ## Output Shape (Important)
286
+
287
+ When `context_schema` is present, the LLM should return the **schema value directly**:
288
+
289
+ - For `string`: `"short summary"`
290
+ - For `integer`: `42`
291
+ - For `object`: `{ "domain": "ECOSYSTEM", "instruction": "..." }`
292
+ - For `list`: `["a", "b", "c"]`
293
+
294
+ SOE stores that value under `context[output_field]`.
295
+
296
+ ## Defining Schemas in Config (Recommended)
297
+
298
+ The simplest approach is including `context_schema` directly in your config YAML:
299
+
300
+ ```yaml
301
+ # Complete config with workflows and context_schema
302
+ workflows:
303
+ example_workflow:
304
+ Summarizer:
305
+ node_type: llm
306
+ event_triggers: [START]
307
+ prompt: "Summarize: {{ context.input }}"
308
+ output_field: summary
309
+ event_emissions:
310
+ - signal_name: DONE
311
+
312
+ context_schema:
313
+ summary:
314
+ type: string
315
+ description: A one-sentence summary
316
+ result:
317
+ type: object
318
+ description: The workflow result
319
+ ```
320
+
321
+ Then pass the entire config to orchestrate:
322
+
323
+ ```python
324
+ from soe import orchestrate
325
+
326
+ execution_id = orchestrate(
327
+ config=CONFIG_YAML, # The YAML string above
328
+ initial_workflow_name="example_workflow",
329
+ initial_signals=["START"],
330
+ initial_context={"input": "test"},
331
+ backends=backends,
332
+ broadcast_signals_caller=broadcast_signals_caller,
333
+ )
334
+ ```
335
+
336
+ When `context_schema` is included in config:
337
+ 1. It's automatically extracted and saved to the `ContextSchemaBackend`
338
+ 2. It's keyed by `execution_id` (specifically `main_execution_id`)
339
+ 3. Child workflows can access parent's schema through the same `main_execution_id`
340
+
341
+ ### Backend Requirement
342
+
343
+ For context schema to work, you need a `ContextSchemaBackend`. The local backends include one:
344
+
345
+ ```python
346
+ from soe.local_backends import create_local_backends
347
+
348
+ backends = create_local_backends(
349
+ context_storage_dir="./data/contexts",
350
+ workflow_storage_dir="./data/workflows",
351
+ schema_storage_dir="./data/schemas", # Context schema storage
352
+ )
353
+ ```
354
+
355
+ **Recommendation**: Use the same database for workflows, context, identities, and context_schema. The backend methods create separate tables, not separate databases. This simplifies infrastructure management.
356
+
357
+ ## Saving Schemas Programmatically
358
+
359
+ You can also save schemas via the backend directly. Note that schemas are keyed by `execution_id`, not workflow name:
360
+
361
+ ```python
362
+ from soe import orchestrate
363
+ from soe.local_backends import create_local_backends
364
+
365
+ backends = create_local_backends(...)
366
+
367
+ # Run orchestrate first to get the execution_id
368
+ execution_id = orchestrate(
369
+ config=MY_WORKFLOW,
370
+ initial_workflow_name="my_workflow",
371
+ initial_signals=["START"],
372
+ initial_context={"input": "test"},
373
+ backends=backends,
374
+ broadcast_signals_caller=broadcast_signals_caller,
375
+ )
376
+
377
+ # Retrieve schema (keyed by execution_id)
378
+ schema = backends.context_schema.get_context_schema(execution_id)
379
+
380
+ # Get schema for specific field
381
+ field_schema = backends.context_schema.get_field_schema(execution_id, "result")
382
+ ```
383
+
384
+ **Important**: The preferred approach is defining `context_schema` in your config, which automatically saves it before orchestration begins.
385
+
386
+ ## Key Points
387
+
388
+ - **Optional but powerful**: Use schemas when type safety matters.
389
+ - **Define in config**: Use `context_schema` section in your config for automatic setup.
390
+ - **Keyed by execution_id**: Schemas are stored by `main_execution_id`, enabling child workflow access.
391
+ - **Per-field types**: Each context field can have its own type.
392
+ - **LLM validation**: Ensures LLM output matches expected structure.
393
+ - **Tool integration**: Critical when LLM output feeds tool parameters.
394
+
395
+ ## Next Steps
396
+
397
+ Now that you understand how to validate LLM output structure, let's explore [Identity](guide_07_identity.md) for persisting conversation history across LLM calls →