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.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +139 -0
  3. data/CODE_OF_CONDUCT.md +128 -0
  4. data/LICENSE +21 -0
  5. data/README.md +226 -0
  6. data/Rakefile +14 -0
  7. data/UPSTREAM_PROPOSAL.md +141 -0
  8. data/docs/CONFIGURATION.md +123 -0
  9. data/docs/PATTERNS.md +492 -0
  10. data/docs/PERSISTENCE.md +169 -0
  11. data/docs/TOOLS_AND_GUARDRAILS.md +140 -0
  12. data/docs/workflow_claim.md +58 -0
  13. data/exe/smith +7 -0
  14. data/lib/generators/smith/install/install_generator.rb +22 -0
  15. data/lib/generators/smith/install/templates/smith.rb.tt +44 -0
  16. data/lib/smith/agent/lifecycle.rb +264 -0
  17. data/lib/smith/agent/registry.rb +128 -0
  18. data/lib/smith/agent.rb +259 -0
  19. data/lib/smith/artifacts/file.rb +59 -0
  20. data/lib/smith/artifacts/memory.rb +75 -0
  21. data/lib/smith/artifacts/scoped_store.rb +29 -0
  22. data/lib/smith/artifacts.rb +5 -0
  23. data/lib/smith/budget/ledger.rb +42 -0
  24. data/lib/smith/budget.rb +5 -0
  25. data/lib/smith/cli.rb +82 -0
  26. data/lib/smith/context/observation_masking.rb +19 -0
  27. data/lib/smith/context/session.rb +42 -0
  28. data/lib/smith/context/state_injection.rb +24 -0
  29. data/lib/smith/context.rb +61 -0
  30. data/lib/smith/doctor/check.rb +12 -0
  31. data/lib/smith/doctor/checks/baseline.rb +84 -0
  32. data/lib/smith/doctor/checks/configuration.rb +56 -0
  33. data/lib/smith/doctor/checks/durability.rb +103 -0
  34. data/lib/smith/doctor/checks/live.rb +55 -0
  35. data/lib/smith/doctor/checks/models_registry.rb +66 -0
  36. data/lib/smith/doctor/checks/openai_api_mode.rb +51 -0
  37. data/lib/smith/doctor/checks/persistence.rb +99 -0
  38. data/lib/smith/doctor/checks/persistence_capabilities.rb +60 -0
  39. data/lib/smith/doctor/checks/persistence_registry.rb +82 -0
  40. data/lib/smith/doctor/checks/rails.rb +39 -0
  41. data/lib/smith/doctor/checks/serialization.rb +78 -0
  42. data/lib/smith/doctor/installer.rb +103 -0
  43. data/lib/smith/doctor/printer.rb +62 -0
  44. data/lib/smith/doctor/report.rb +39 -0
  45. data/lib/smith/doctor.rb +53 -0
  46. data/lib/smith/errors.rb +191 -0
  47. data/lib/smith/event.rb +11 -0
  48. data/lib/smith/events/.keep +0 -0
  49. data/lib/smith/events/bus.rb +60 -0
  50. data/lib/smith/events/step_completed.rb +11 -0
  51. data/lib/smith/events/subscription.rb +24 -0
  52. data/lib/smith/events.rb +5 -0
  53. data/lib/smith/guardrails/runner.rb +44 -0
  54. data/lib/smith/guardrails/url_verifier.rb +7 -0
  55. data/lib/smith/guardrails.rb +35 -0
  56. data/lib/smith/models/inference.rb +199 -0
  57. data/lib/smith/models/normalizer.rb +186 -0
  58. data/lib/smith/models/profile.rb +39 -0
  59. data/lib/smith/models.rb +132 -0
  60. data/lib/smith/persistence_adapters/active_record_store.rb +99 -0
  61. data/lib/smith/persistence_adapters/cache_store.rb +79 -0
  62. data/lib/smith/persistence_adapters/memory.rb +105 -0
  63. data/lib/smith/persistence_adapters/rails_cache.rb +20 -0
  64. data/lib/smith/persistence_adapters/redis_store.rb +136 -0
  65. data/lib/smith/persistence_adapters/retry.rb +42 -0
  66. data/lib/smith/persistence_adapters.rb +112 -0
  67. data/lib/smith/pricing.rb +65 -0
  68. data/lib/smith/providers/openai/responses.rb +315 -0
  69. data/lib/smith/providers/openai/routing.rb +67 -0
  70. data/lib/smith/providers/openai/tools_extensions.rb +106 -0
  71. data/lib/smith/railtie.rb +9 -0
  72. data/lib/smith/tasks/doctor.rake +38 -0
  73. data/lib/smith/tool/budget_enforcement.rb +33 -0
  74. data/lib/smith/tool/capability_builder.rb +18 -0
  75. data/lib/smith/tool/capture.rb +22 -0
  76. data/lib/smith/tool/compatibility.rb +72 -0
  77. data/lib/smith/tool/policy.rb +40 -0
  78. data/lib/smith/tool.rb +171 -0
  79. data/lib/smith/tools/think.rb +25 -0
  80. data/lib/smith/tools/url_fetcher.rb +16 -0
  81. data/lib/smith/tools/web_search.rb +17 -0
  82. data/lib/smith/tools.rb +5 -0
  83. data/lib/smith/trace/logger.rb +46 -0
  84. data/lib/smith/trace/memory.rb +53 -0
  85. data/lib/smith/trace/open_telemetry.rb +57 -0
  86. data/lib/smith/trace.rb +89 -0
  87. data/lib/smith/types.rb +16 -0
  88. data/lib/smith/version.rb +5 -0
  89. data/lib/smith/workflow/artifact_integration.rb +41 -0
  90. data/lib/smith/workflow/budget_integration.rb +105 -0
  91. data/lib/smith/workflow/claim.rb +118 -0
  92. data/lib/smith/workflow/data_volume_policy.rb +36 -0
  93. data/lib/smith/workflow/deadline_enforcement.rb +100 -0
  94. data/lib/smith/workflow/deterministic_execution.rb +53 -0
  95. data/lib/smith/workflow/deterministic_step.rb +57 -0
  96. data/lib/smith/workflow/dsl.rb +223 -0
  97. data/lib/smith/workflow/durability.rb +369 -0
  98. data/lib/smith/workflow/evaluator_optimizer.rb +220 -0
  99. data/lib/smith/workflow/event_integration.rb +24 -0
  100. data/lib/smith/workflow/execution.rb +127 -0
  101. data/lib/smith/workflow/execution_frame.rb +166 -0
  102. data/lib/smith/workflow/guardrail_integration.rb +40 -0
  103. data/lib/smith/workflow/nested_execution.rb +69 -0
  104. data/lib/smith/workflow/orchestrator_worker.rb +145 -0
  105. data/lib/smith/workflow/parallel.rb +50 -0
  106. data/lib/smith/workflow/parallel_execution.rb +75 -0
  107. data/lib/smith/workflow/persistence.rb +358 -0
  108. data/lib/smith/workflow/pipeline.rb +117 -0
  109. data/lib/smith/workflow/router.rb +53 -0
  110. data/lib/smith/workflow/transition.rb +208 -0
  111. data/lib/smith/workflow.rb +555 -0
  112. data/lib/smith.rb +254 -0
  113. data/script/profile_tool_results.rb +94 -0
  114. data/sig/smith.rbs +4 -0
  115. 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
+
@@ -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
+