smith-agents 0.4.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +139 -0
- data/CODE_OF_CONDUCT.md +128 -0
- data/LICENSE +21 -0
- data/README.md +226 -0
- data/Rakefile +14 -0
- data/UPSTREAM_PROPOSAL.md +141 -0
- data/docs/CONFIGURATION.md +123 -0
- data/docs/PATTERNS.md +492 -0
- data/docs/PERSISTENCE.md +169 -0
- data/docs/TOOLS_AND_GUARDRAILS.md +140 -0
- data/docs/workflow_claim.md +58 -0
- data/exe/smith +7 -0
- data/lib/generators/smith/install/install_generator.rb +22 -0
- data/lib/generators/smith/install/templates/smith.rb.tt +44 -0
- data/lib/smith/agent/lifecycle.rb +264 -0
- data/lib/smith/agent/registry.rb +128 -0
- data/lib/smith/agent.rb +259 -0
- data/lib/smith/artifacts/file.rb +59 -0
- data/lib/smith/artifacts/memory.rb +75 -0
- data/lib/smith/artifacts/scoped_store.rb +29 -0
- data/lib/smith/artifacts.rb +5 -0
- data/lib/smith/budget/ledger.rb +42 -0
- data/lib/smith/budget.rb +5 -0
- data/lib/smith/cli.rb +82 -0
- data/lib/smith/context/observation_masking.rb +19 -0
- data/lib/smith/context/session.rb +42 -0
- data/lib/smith/context/state_injection.rb +24 -0
- data/lib/smith/context.rb +61 -0
- data/lib/smith/doctor/check.rb +12 -0
- data/lib/smith/doctor/checks/baseline.rb +84 -0
- data/lib/smith/doctor/checks/configuration.rb +56 -0
- data/lib/smith/doctor/checks/durability.rb +103 -0
- data/lib/smith/doctor/checks/live.rb +55 -0
- data/lib/smith/doctor/checks/models_registry.rb +66 -0
- data/lib/smith/doctor/checks/openai_api_mode.rb +51 -0
- data/lib/smith/doctor/checks/persistence.rb +99 -0
- data/lib/smith/doctor/checks/persistence_capabilities.rb +60 -0
- data/lib/smith/doctor/checks/persistence_registry.rb +82 -0
- data/lib/smith/doctor/checks/rails.rb +39 -0
- data/lib/smith/doctor/checks/serialization.rb +78 -0
- data/lib/smith/doctor/installer.rb +103 -0
- data/lib/smith/doctor/printer.rb +62 -0
- data/lib/smith/doctor/report.rb +39 -0
- data/lib/smith/doctor.rb +53 -0
- data/lib/smith/errors.rb +191 -0
- data/lib/smith/event.rb +11 -0
- data/lib/smith/events/.keep +0 -0
- data/lib/smith/events/bus.rb +60 -0
- data/lib/smith/events/step_completed.rb +11 -0
- data/lib/smith/events/subscription.rb +24 -0
- data/lib/smith/events.rb +5 -0
- data/lib/smith/guardrails/runner.rb +44 -0
- data/lib/smith/guardrails/url_verifier.rb +7 -0
- data/lib/smith/guardrails.rb +35 -0
- data/lib/smith/models/inference.rb +199 -0
- data/lib/smith/models/normalizer.rb +186 -0
- data/lib/smith/models/profile.rb +39 -0
- data/lib/smith/models.rb +132 -0
- data/lib/smith/persistence_adapters/active_record_store.rb +99 -0
- data/lib/smith/persistence_adapters/cache_store.rb +79 -0
- data/lib/smith/persistence_adapters/memory.rb +105 -0
- data/lib/smith/persistence_adapters/rails_cache.rb +20 -0
- data/lib/smith/persistence_adapters/redis_store.rb +136 -0
- data/lib/smith/persistence_adapters/retry.rb +42 -0
- data/lib/smith/persistence_adapters.rb +112 -0
- data/lib/smith/pricing.rb +65 -0
- data/lib/smith/providers/openai/responses.rb +315 -0
- data/lib/smith/providers/openai/routing.rb +67 -0
- data/lib/smith/providers/openai/tools_extensions.rb +106 -0
- data/lib/smith/railtie.rb +9 -0
- data/lib/smith/tasks/doctor.rake +38 -0
- data/lib/smith/tool/budget_enforcement.rb +33 -0
- data/lib/smith/tool/capability_builder.rb +18 -0
- data/lib/smith/tool/capture.rb +22 -0
- data/lib/smith/tool/compatibility.rb +72 -0
- data/lib/smith/tool/policy.rb +40 -0
- data/lib/smith/tool.rb +171 -0
- data/lib/smith/tools/think.rb +25 -0
- data/lib/smith/tools/url_fetcher.rb +16 -0
- data/lib/smith/tools/web_search.rb +17 -0
- data/lib/smith/tools.rb +5 -0
- data/lib/smith/trace/logger.rb +46 -0
- data/lib/smith/trace/memory.rb +53 -0
- data/lib/smith/trace/open_telemetry.rb +57 -0
- data/lib/smith/trace.rb +89 -0
- data/lib/smith/types.rb +16 -0
- data/lib/smith/version.rb +5 -0
- data/lib/smith/workflow/artifact_integration.rb +41 -0
- data/lib/smith/workflow/budget_integration.rb +105 -0
- data/lib/smith/workflow/claim.rb +118 -0
- data/lib/smith/workflow/data_volume_policy.rb +36 -0
- data/lib/smith/workflow/deadline_enforcement.rb +100 -0
- data/lib/smith/workflow/deterministic_execution.rb +53 -0
- data/lib/smith/workflow/deterministic_step.rb +57 -0
- data/lib/smith/workflow/dsl.rb +223 -0
- data/lib/smith/workflow/durability.rb +369 -0
- data/lib/smith/workflow/evaluator_optimizer.rb +220 -0
- data/lib/smith/workflow/event_integration.rb +24 -0
- data/lib/smith/workflow/execution.rb +127 -0
- data/lib/smith/workflow/execution_frame.rb +166 -0
- data/lib/smith/workflow/guardrail_integration.rb +40 -0
- data/lib/smith/workflow/nested_execution.rb +69 -0
- data/lib/smith/workflow/orchestrator_worker.rb +145 -0
- data/lib/smith/workflow/parallel.rb +50 -0
- data/lib/smith/workflow/parallel_execution.rb +75 -0
- data/lib/smith/workflow/persistence.rb +358 -0
- data/lib/smith/workflow/pipeline.rb +117 -0
- data/lib/smith/workflow/router.rb +53 -0
- data/lib/smith/workflow/transition.rb +208 -0
- data/lib/smith/workflow.rb +555 -0
- data/lib/smith.rb +254 -0
- data/script/profile_tool_results.rb +94 -0
- data/sig/smith.rbs +4 -0
- metadata +258 -0
data/docs/PATTERNS.md
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
# Patterns
|
|
2
|
+
|
|
3
|
+
Working examples for each Smith workflow pattern. The table in the [README](../README.md#patterns) is the quick selection rule; this file is the depth.
|
|
4
|
+
|
|
5
|
+
## Example 1: Single-Step Workflow
|
|
6
|
+
|
|
7
|
+
Use this when you want one agent call with real workflow semantics around it.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class TicketReplyAgent < Smith::Agent
|
|
11
|
+
register_as :ticket_reply_agent
|
|
12
|
+
model "gpt-4.1-nano"
|
|
13
|
+
|
|
14
|
+
instructions do |_context|
|
|
15
|
+
"Draft a support reply that is concise, calm, and actionable."
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class TicketReplyWorkflow < Smith::Workflow
|
|
20
|
+
initial_state :idle
|
|
21
|
+
state :done
|
|
22
|
+
state :failed
|
|
23
|
+
|
|
24
|
+
transition :reply, from: :idle, to: :done do
|
|
25
|
+
execute :ticket_reply_agent
|
|
26
|
+
on_failure :fail
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
result = TicketReplyWorkflow.new.run!
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Why this is useful even when it looks small:
|
|
34
|
+
|
|
35
|
+
- you get a named transition
|
|
36
|
+
- failures route consistently
|
|
37
|
+
- the step is visible in `result.steps`
|
|
38
|
+
- you can later add budgets, guardrails, persistence, context, or tracing without rewriting the shape
|
|
39
|
+
|
|
40
|
+
## Example 2: Multi-Step Workflow With Explicit Success Paths
|
|
41
|
+
|
|
42
|
+
Use this when you want sequential work, but each stage still needs its own step boundary and failure semantics.
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
class IntakeAgent < Smith::Agent
|
|
46
|
+
register_as :intake_agent
|
|
47
|
+
model "gpt-4.1-nano"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class DraftAgent < Smith::Agent
|
|
51
|
+
register_as :draft_agent
|
|
52
|
+
model "gpt-4.1-nano"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class ReviewWorkflow < Smith::Workflow
|
|
56
|
+
initial_state :idle
|
|
57
|
+
state :triaged
|
|
58
|
+
state :drafted
|
|
59
|
+
state :done
|
|
60
|
+
state :failed
|
|
61
|
+
|
|
62
|
+
transition :intake, from: :idle, to: :triaged do
|
|
63
|
+
execute :intake_agent
|
|
64
|
+
on_success :draft
|
|
65
|
+
on_failure :fail
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
transition :draft, from: :triaged, to: :drafted do
|
|
69
|
+
execute :draft_agent
|
|
70
|
+
on_success :finish
|
|
71
|
+
on_failure :fail
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
transition :finish, from: :drafted, to: :done
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Value:
|
|
79
|
+
|
|
80
|
+
- no hidden control flow
|
|
81
|
+
- no prompt-level "now do step 2"
|
|
82
|
+
- if step 1 or step 2 fails, the failure is a real workflow event, not an accidental provider exception leaking through
|
|
83
|
+
|
|
84
|
+
## Example 3: Pipeline
|
|
85
|
+
|
|
86
|
+
Use `pipeline` when the flow is mechanically sequential and you do not want to hand-write each transition.
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
class ResearchAgent < Smith::Agent
|
|
90
|
+
register_as :research_agent
|
|
91
|
+
model "gpt-4.1-nano"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
class OutlineAgent < Smith::Agent
|
|
95
|
+
register_as :outline_agent
|
|
96
|
+
model "gpt-4.1-nano"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class DraftAgent < Smith::Agent
|
|
100
|
+
register_as :draft_agent
|
|
101
|
+
model "gpt-4.1-nano"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class ArticleWorkflow < Smith::Workflow
|
|
105
|
+
initial_state :idle
|
|
106
|
+
state :drafted
|
|
107
|
+
state :failed
|
|
108
|
+
|
|
109
|
+
pipeline :draft_article, from: :idle, to: :drafted do
|
|
110
|
+
stage :research, execute: :research_agent
|
|
111
|
+
stage :outline, execute: :outline_agent
|
|
112
|
+
stage :draft, execute: :draft_agent
|
|
113
|
+
on_failure :fail
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Why pipeline matters:
|
|
119
|
+
|
|
120
|
+
- you still get real step boundaries
|
|
121
|
+
- each stage is still visible in the step log
|
|
122
|
+
- the last stage output becomes the workflow result
|
|
123
|
+
- the generated transitions are explicit and stable, rather than hidden in a loop
|
|
124
|
+
|
|
125
|
+
Note: `on_failure` inside the `pipeline` block applies to the generated pipeline transitions as a whole.
|
|
126
|
+
It is not a separate per-stage custom failure policy surface.
|
|
127
|
+
|
|
128
|
+
## Example 4: Router
|
|
129
|
+
|
|
130
|
+
Use `route` when a classifier decides which specialist transition should run next.
|
|
131
|
+
|
|
132
|
+
The classifier output must be a hash that includes:
|
|
133
|
+
|
|
134
|
+
- `:route`
|
|
135
|
+
- `:confidence`
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
class RouteDecisionSchema
|
|
141
|
+
# Replace this with your real RubyLLM schema object/class.
|
|
142
|
+
# Intended shape:
|
|
143
|
+
# { route: :refund, confidence: 0.91 }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
class TriageAgent < Smith::Agent
|
|
147
|
+
register_as :triage_agent
|
|
148
|
+
model "gpt-4.1-nano"
|
|
149
|
+
output_schema RouteDecisionSchema
|
|
150
|
+
|
|
151
|
+
instructions do |_context|
|
|
152
|
+
<<~TEXT
|
|
153
|
+
Return a Hash with:
|
|
154
|
+
- :route => one of the declared route keys
|
|
155
|
+
- :confidence => a float between 0.0 and 1.0
|
|
156
|
+
TEXT
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
class RefundAgent < Smith::Agent
|
|
161
|
+
register_as :refund_agent
|
|
162
|
+
model "gpt-4.1-nano"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
class GeneralSupportAgent < Smith::Agent
|
|
166
|
+
register_as :general_support_agent
|
|
167
|
+
model "gpt-4.1-nano"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
class SupportRouterWorkflow < Smith::Workflow
|
|
171
|
+
initial_state :idle
|
|
172
|
+
state :triaged
|
|
173
|
+
state :refund_handled
|
|
174
|
+
state :general_handled
|
|
175
|
+
state :failed
|
|
176
|
+
|
|
177
|
+
transition :classify, from: :idle, to: :triaged do
|
|
178
|
+
route :triage_agent,
|
|
179
|
+
routes: {
|
|
180
|
+
refund: :handle_refund,
|
|
181
|
+
support: :handle_general
|
|
182
|
+
},
|
|
183
|
+
confidence_threshold: 0.75,
|
|
184
|
+
fallback: :handle_general
|
|
185
|
+
on_failure :fail
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
transition :handle_refund, from: :triaged, to: :refund_handled do
|
|
189
|
+
execute :refund_agent
|
|
190
|
+
on_failure :fail
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
transition :handle_general, from: :triaged, to: :general_handled do
|
|
194
|
+
execute :general_support_agent
|
|
195
|
+
on_failure :fail
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Why this is better than "classifier prompt + if/else outside":
|
|
201
|
+
|
|
202
|
+
- route resolution is part of the workflow contract
|
|
203
|
+
- confidence thresholds are explicit
|
|
204
|
+
- invalid router outputs fail as workflow errors
|
|
205
|
+
- the chosen next transition is persisted and restored across resume
|
|
206
|
+
|
|
207
|
+
In practice, router outputs should be treated as structured outputs, not free-form prose.
|
|
208
|
+
|
|
209
|
+
## Example 5: Parallel Fan-Out
|
|
210
|
+
|
|
211
|
+
Use parallel execution when the same kind of work must be done across multiple branches.
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
class FindingAgent < Smith::Agent
|
|
215
|
+
register_as :finding_agent
|
|
216
|
+
model "gpt-4.1-nano"
|
|
217
|
+
budget token_limit: 8_000, cost: 0.20, wall_clock: 15
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
class ParallelResearchWorkflow < Smith::Workflow
|
|
221
|
+
initial_state :idle
|
|
222
|
+
state :done
|
|
223
|
+
state :failed
|
|
224
|
+
|
|
225
|
+
budget total_tokens: 60_000, total_cost: 1.50, wall_clock: 90
|
|
226
|
+
|
|
227
|
+
transition :fan_out, from: :idle, to: :done do
|
|
228
|
+
execute :finding_agent, parallel: true, count: 4
|
|
229
|
+
on_failure :fail
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Why this is valuable:
|
|
235
|
+
|
|
236
|
+
- Smith treats each branch as a real invocation
|
|
237
|
+
- workflow budgets remain cumulative outer limits
|
|
238
|
+
- agent budgets still narrow each branch call
|
|
239
|
+
- branch failures discard step output and route through normal failure handling
|
|
240
|
+
- prepared input is reused consistently across branches
|
|
241
|
+
|
|
242
|
+
## Example 6: Nested Workflows
|
|
243
|
+
|
|
244
|
+
Use nested workflows when one part of the system deserves to be a reusable subflow with its own states and transitions.
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
class ChildResearchAgent < Smith::Agent
|
|
248
|
+
register_as :child_research_agent
|
|
249
|
+
model "gpt-4.1-nano"
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
class ResearchSubflow < Smith::Workflow
|
|
253
|
+
initial_state :idle
|
|
254
|
+
state :done
|
|
255
|
+
|
|
256
|
+
transition :research, from: :idle, to: :done do
|
|
257
|
+
execute :child_research_agent
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
class ParentWorkflow < Smith::Workflow
|
|
262
|
+
initial_state :idle
|
|
263
|
+
state :researched
|
|
264
|
+
state :done
|
|
265
|
+
state :failed
|
|
266
|
+
|
|
267
|
+
transition :run_research, from: :idle, to: :researched do
|
|
268
|
+
workflow ResearchSubflow
|
|
269
|
+
on_failure :fail
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
transition :finish, from: :researched, to: :done
|
|
273
|
+
end
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
What you get:
|
|
277
|
+
|
|
278
|
+
- the child workflow's final output becomes the parent step output
|
|
279
|
+
- parent step count stays parent-scoped
|
|
280
|
+
- parent and child share the outer budget ledger
|
|
281
|
+
- nested best-known token/cost totals roll up into the parent result
|
|
282
|
+
- artifact scope is preserved across nesting
|
|
283
|
+
|
|
284
|
+
## Example 7: Evaluator-Optimizer
|
|
285
|
+
|
|
286
|
+
Use `optimize` when one agent generates candidates and another agent evaluates whether the result is acceptable.
|
|
287
|
+
|
|
288
|
+
The evaluator output is expected to carry a contract like:
|
|
289
|
+
|
|
290
|
+
- `accept: true/false`
|
|
291
|
+
- `feedback: ...` when rejecting
|
|
292
|
+
- optional `score`
|
|
293
|
+
- optional `converged`
|
|
294
|
+
|
|
295
|
+
Example:
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
class TranslationEvaluationSchema
|
|
299
|
+
# Replace this with your real RubyLLM schema object/class.
|
|
300
|
+
# Intended shape:
|
|
301
|
+
# { accept: true/false, feedback: "...", score: 0.93 }
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
class TranslationGenerator < Smith::Agent
|
|
305
|
+
register_as :translation_generator
|
|
306
|
+
model "gpt-4.1-nano"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
class TranslationEvaluator < Smith::Agent
|
|
310
|
+
register_as :translation_evaluator
|
|
311
|
+
model "gpt-4.1-nano"
|
|
312
|
+
output_schema TranslationEvaluationSchema
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
class TranslationWorkflow < Smith::Workflow
|
|
316
|
+
initial_state :idle
|
|
317
|
+
state :done
|
|
318
|
+
state :failed
|
|
319
|
+
|
|
320
|
+
transition :translate, from: :idle, to: :done do
|
|
321
|
+
optimize generator: :translation_generator,
|
|
322
|
+
evaluator: :translation_evaluator,
|
|
323
|
+
max_rounds: 3,
|
|
324
|
+
evaluator_schema: TranslationEvaluationSchema,
|
|
325
|
+
improvement_threshold: 0.05
|
|
326
|
+
on_failure :fail
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Why this matters:
|
|
332
|
+
|
|
333
|
+
- the loop is explicit, bounded, and observable
|
|
334
|
+
- acceptance criteria are structured
|
|
335
|
+
- exhaustion, malformed evaluator output, and convergence without acceptance fail normally
|
|
336
|
+
- costs and token usage from the full loop roll into the workflow totals
|
|
337
|
+
|
|
338
|
+
## Example 8: Orchestrator-Worker
|
|
339
|
+
|
|
340
|
+
Use `orchestrate` when you need an orchestrator that can emit structured tasks for workers and later decide when the system is done.
|
|
341
|
+
|
|
342
|
+
The orchestrator can emit one of:
|
|
343
|
+
|
|
344
|
+
- `tasks: [...]`
|
|
345
|
+
- `final: {...}`
|
|
346
|
+
- `stop: "...reason..."`
|
|
347
|
+
|
|
348
|
+
Example schemas:
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
class ResearchTaskSchema
|
|
352
|
+
def self.required_keys = %i[task_id input]
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
class WorkerOutputSchema
|
|
356
|
+
def self.required_keys = %i[finding]
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
class FinalOutputSchema
|
|
360
|
+
def self.required_keys = %i[summary]
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
class OrchestratorDecisionSchema
|
|
364
|
+
# Replace this with your real RubyLLM schema object/class.
|
|
365
|
+
# Intended shape:
|
|
366
|
+
# { tasks: [...] } or { final: {...} } or { stop: "..." }
|
|
367
|
+
end
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Example workflow:
|
|
371
|
+
|
|
372
|
+
```ruby
|
|
373
|
+
class ResearchOrchestrator < Smith::Agent
|
|
374
|
+
register_as :research_orchestrator
|
|
375
|
+
model "gpt-4.1-nano"
|
|
376
|
+
output_schema OrchestratorDecisionSchema
|
|
377
|
+
|
|
378
|
+
instructions do |_context|
|
|
379
|
+
<<~TEXT
|
|
380
|
+
Return exactly one of:
|
|
381
|
+
- { tasks: [{ task_id:, input: }] }
|
|
382
|
+
- { final: { summary: ... } }
|
|
383
|
+
- { stop: "reason" }
|
|
384
|
+
TEXT
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
class ResearchWorker < Smith::Agent
|
|
389
|
+
register_as :research_worker
|
|
390
|
+
model "gpt-4.1-nano"
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
class ResearchProgramWorkflow < Smith::Workflow
|
|
394
|
+
initial_state :idle
|
|
395
|
+
state :done
|
|
396
|
+
state :failed
|
|
397
|
+
|
|
398
|
+
transition :research, from: :idle, to: :done do
|
|
399
|
+
orchestrate orchestrator: :research_orchestrator,
|
|
400
|
+
worker: :research_worker,
|
|
401
|
+
max_workers: 4,
|
|
402
|
+
max_delegation_rounds: 3,
|
|
403
|
+
task_schema: ResearchTaskSchema,
|
|
404
|
+
worker_output_schema: WorkerOutputSchema,
|
|
405
|
+
final_output_schema: FinalOutputSchema
|
|
406
|
+
on_failure :fail
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
Why this is valuable:
|
|
412
|
+
|
|
413
|
+
- delegation is explicit and bounded
|
|
414
|
+
- tasks and outputs are structured
|
|
415
|
+
- worker fan-out is controlled
|
|
416
|
+
- exhaustion and malformed orchestrator output fail as first-class workflow failures
|
|
417
|
+
|
|
418
|
+
Notes:
|
|
419
|
+
|
|
420
|
+
- the workflow helper validates `task_schema`, `worker_output_schema`, and `final_output_schema`
|
|
421
|
+
- worker execution automatically applies `worker_output_schema`
|
|
422
|
+
- the orchestrator still benefits from `output_schema` so its decision shape is pushed down to the provider layer too
|
|
423
|
+
|
|
424
|
+
## Deterministic Steps
|
|
425
|
+
|
|
426
|
+
Not every workflow step needs an agent. Sometimes you need small, deterministic logic inside the graph: verification, routing, normalization, or failure classification. Smith provides two transition primitives for this: `compute` and `run`.
|
|
427
|
+
|
|
428
|
+
Both yield a constrained step object — not the full workflow — and execute synchronously with no agent call, no budget consumption, and no session message output.
|
|
429
|
+
|
|
430
|
+
### `compute` — Verification and Routing
|
|
431
|
+
|
|
432
|
+
Use `compute` for steps that check prior output and decide what happens next.
|
|
433
|
+
|
|
434
|
+
```ruby
|
|
435
|
+
transition :verify_research, from: :gathered, to: :verified do
|
|
436
|
+
compute do |step|
|
|
437
|
+
if step.tool_results.any? { |t| t[:captured]&.dig(:retryable) }
|
|
438
|
+
step.fail!("research temporarily unavailable", retryable: true)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
unless step.last_output
|
|
442
|
+
step.write_outcome(kind: :terminal_failure, payload: { message: "no usable research output" })
|
|
443
|
+
step.route_to(:finish_terminal_failure)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
step.route_to(:structure)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
on_failure :fail
|
|
450
|
+
end
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### `run` — Normalization and Context Shaping
|
|
454
|
+
|
|
455
|
+
Use `run` for steps that transform or prepare workflow-local state.
|
|
456
|
+
|
|
457
|
+
```ruby
|
|
458
|
+
transition :normalize, from: :gathered, to: :prepared do
|
|
459
|
+
run do |step|
|
|
460
|
+
step.write_context(:normalized, step.last_output&.upcase)
|
|
461
|
+
step.route_to(:structure)
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
### Step Object API
|
|
467
|
+
|
|
468
|
+
The yielded step object exposes a narrow, read-heavy surface:
|
|
469
|
+
|
|
470
|
+
| Read | Write / Control |
|
|
471
|
+
|---|---|
|
|
472
|
+
| `step.context` | `step.write_context(key, value)` |
|
|
473
|
+
| `step.read_context(key)` | `step.write_outcome(kind:, payload:)` |
|
|
474
|
+
| `step.last_output` / `step.output` | `step.route_to(:transition_name)` |
|
|
475
|
+
| | `step.fail!(msg, retryable:, kind:, details:)` |
|
|
476
|
+
| `step.tool_results` | |
|
|
477
|
+
| `step.session_messages` | |
|
|
478
|
+
| `step.current_state` | |
|
|
479
|
+
| `step.transition_name` | |
|
|
480
|
+
|
|
481
|
+
### Behavior
|
|
482
|
+
|
|
483
|
+
- **Routing**: `step.route_to` overrides `on_success`. If neither is set, normal state-based resolution applies. Named transitions that do not exist fail loudly with `WorkflowError`.
|
|
484
|
+
- **Failure**: `step.fail!` raises `Smith::DeterministicStepFailure` (extends `WorkflowError`) with `retryable`, `kind`, and `details` metadata. Routes through `on_failure` like any other step failure.
|
|
485
|
+
- **Outcome**: `step.write_outcome(kind:, payload:)` stores a workflow-owned terminal payload without smuggling it through context. The payload is persisted with the workflow and surfaced on `RunResult.outcome`, `RunResult.outcome_kind`, and `RunResult.outcome_payload`.
|
|
486
|
+
- **Context reads**: `step.context` returns an isolated snapshot of the workflow context at step start. Mutating that snapshot does not mutate workflow state. `step.read_context(key)` returns a merged view — pending `write_context` values override the snapshot. Use `read_context` when you need read-after-write coherence within the same step.
|
|
487
|
+
- **No output**: Deterministic steps produce no session message output. `last_output` continues to mean the last agent output.
|
|
488
|
+
- **No budget**: No tokens or cost consumed.
|
|
489
|
+
- **Persistence**: Context writes and written outcomes survive `to_state`/`from_state`. The block itself (a Proc) lives on the class-level Transition and is never serialized.
|
|
490
|
+
- **Trace**: Emits `:deterministic_step` traces for start, success/routed, and failure. When a step writes an outcome, the trace includes `outcome_kind`.
|
|
491
|
+
- **Mutual exclusivity**: `compute` and `run` cannot be combined with `execute`, `route`, `workflow`, `optimize`, or `orchestrate`. A transition declares exactly one primary execution body.
|
|
492
|
+
|
data/docs/PERSISTENCE.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Context, Session History, and Resume
|
|
2
|
+
|
|
3
|
+
Use `Smith::Context` when you want:
|
|
4
|
+
|
|
5
|
+
- persisted workflow context keys
|
|
6
|
+
- observation masking over session history
|
|
7
|
+
- injected state summaries
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
class ReviewContext < Smith::Context
|
|
13
|
+
persist :ticket_id, :current_findings, :source_urls
|
|
14
|
+
|
|
15
|
+
session_strategy :observation_masking, window: 6
|
|
16
|
+
|
|
17
|
+
inject_state do |persisted|
|
|
18
|
+
<<~TEXT
|
|
19
|
+
Ticket: #{persisted[:ticket_id]}
|
|
20
|
+
Findings: #{persisted[:current_findings]}
|
|
21
|
+
Sources: #{Array(persisted[:source_urls]).join(", ")}
|
|
22
|
+
TEXT
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class ReviewWorkflow < Smith::Workflow
|
|
27
|
+
context_manager ReviewContext
|
|
28
|
+
initial_state :idle
|
|
29
|
+
state :done
|
|
30
|
+
|
|
31
|
+
transition :review, from: :idle, to: :done do
|
|
32
|
+
execute :review_agent
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
What Smith does for you:
|
|
38
|
+
|
|
39
|
+
- prepares masked session input at step boundaries
|
|
40
|
+
- injects a state summary message into that prepared input
|
|
41
|
+
- persists declared workflow context keys
|
|
42
|
+
- persists accepted session history
|
|
43
|
+
- preserves chosen next transitions across persistence
|
|
44
|
+
- supports JSON host round-trips through `to_state` and `.from_state`
|
|
45
|
+
|
|
46
|
+
Example host-controlled persistence:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
workflow = ReviewWorkflow.new(context: {
|
|
50
|
+
ticket_id: "T-1042",
|
|
51
|
+
current_findings: "needs escalation",
|
|
52
|
+
source_urls: ["https://example.test/refund-policy"]
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
payload = JSON.generate(workflow.to_state)
|
|
56
|
+
|
|
57
|
+
# Store payload wherever your app wants.
|
|
58
|
+
|
|
59
|
+
restored = ReviewWorkflow.from_state(JSON.parse(payload))
|
|
60
|
+
result = restored.run!
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Important: Smith is resumable, but it is still your app's job to store and retrieve that state.
|
|
64
|
+
|
|
65
|
+
For the common restore-or-initialize case, Smith also exposes a small configured-adapter one-liner:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
result = ReviewWorkflow.run_persisted!(
|
|
69
|
+
key: "ticket:T-1042",
|
|
70
|
+
context: {
|
|
71
|
+
ticket_id: "T-1042",
|
|
72
|
+
current_findings: "needs escalation"
|
|
73
|
+
},
|
|
74
|
+
on_step: ->(step) { puts "checkpointed #{step[:transition]}" },
|
|
75
|
+
clear: :done
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`clear: :done` is the default. Pass `clear: false` to preserve terminal state for host-managed cleanup timing, or `clear: :terminal` to clear any terminal workflow state once the run completes.
|
|
80
|
+
|
|
81
|
+
`on_step:` is a best-effort host callback. It runs after an accepted step has been checkpointed. Callback failures are logged and ignored; they do not roll back or abort durable workflow progression.
|
|
82
|
+
|
|
83
|
+
If the persistence key is a deterministic function of workflow context, declare it once on the workflow:
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
class ReviewWorkflow < Smith::Workflow
|
|
87
|
+
persistence_key { |ctx| "ticket:#{ctx[:ticket_id]}" }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
result = ReviewWorkflow.run_persisted!(
|
|
91
|
+
context: {
|
|
92
|
+
ticket_id: "T-1042",
|
|
93
|
+
current_findings: "needs escalation"
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
When a workflow derives its key this way, Smith persists the resolved durability key in workflow state. That keeps instance-level helpers such as `persist!`, `advance_persisted!`, and `clear_persisted!` stable across restore even when the workflow's context manager persists only a filtered subset of context keys.
|
|
99
|
+
|
|
100
|
+
If you need more explicit control, the lower-level lifecycle is still available:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
workflow = ReviewWorkflow.restore_or_initialize(
|
|
104
|
+
key: "ticket:T-1042",
|
|
105
|
+
context: {
|
|
106
|
+
ticket_id: "T-1042",
|
|
107
|
+
current_findings: "needs escalation"
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
step = workflow.advance_persisted!("ticket:T-1042")
|
|
112
|
+
# Host app can broadcast or project progress here.
|
|
113
|
+
emit_progress(step)
|
|
114
|
+
|
|
115
|
+
result = workflow.run_persisted!("ticket:T-1042")
|
|
116
|
+
workflow.clear_persisted!("ticket:T-1042")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`restore(key, ...)` is intentionally stricter: it requires a non-blank explicit key, and the lookup key remains authoritative for the restored workflow even if stored state contains an embedded `persistence_key`.
|
|
120
|
+
|
|
121
|
+
These helpers do not make Smith a job system or durable runtime. They only remove repetitive restore/checkpoint boilerplate around the configured persistence adapter while leaving queueing, projection, and recovery policy with the host app.
|
|
122
|
+
|
|
123
|
+
## Artifacts
|
|
124
|
+
|
|
125
|
+
Use artifacts when outputs are too large to keep inline.
|
|
126
|
+
|
|
127
|
+
Smith exposes:
|
|
128
|
+
|
|
129
|
+
- `Smith.artifacts.store`
|
|
130
|
+
- `Smith.artifacts.fetch`
|
|
131
|
+
- `Smith.artifacts.expired`
|
|
132
|
+
|
|
133
|
+
The common pattern is to hand off the heavy payload in `after_completion`.
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
class LargeReportAgent < Smith::Agent
|
|
137
|
+
register_as :large_report_agent
|
|
138
|
+
model "gpt-4.1-nano"
|
|
139
|
+
data_volume :unbounded
|
|
140
|
+
|
|
141
|
+
def after_completion(result, _context)
|
|
142
|
+
ref = Smith.artifacts.store(
|
|
143
|
+
result[:full_report],
|
|
144
|
+
content_type: "application/json"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
{
|
|
148
|
+
report_ref: ref,
|
|
149
|
+
summary: result[:summary]
|
|
150
|
+
}
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Configure a backend:
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
Smith.configure do |config|
|
|
159
|
+
config.artifact_store = Smith::Artifacts::Memory.new
|
|
160
|
+
config.artifact_retention = 3600
|
|
161
|
+
end
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Why this matters:
|
|
165
|
+
|
|
166
|
+
- large payloads can move out of the inline workflow result
|
|
167
|
+
- refs are execution-scoped
|
|
168
|
+
- nested workflows inherit artifact scope correctly
|
|
169
|
+
|