smith-agents 0.4.2 → 0.4.3

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +55 -3
  3. data/README.md +45 -2
  4. data/UPSTREAM_PROPOSAL.md +19 -0
  5. data/docs/PATTERNS.md +41 -1
  6. data/lib/smith/agent/registry/introspection.rb +27 -0
  7. data/lib/smith/agent/registry.rb +22 -17
  8. data/lib/smith/agent/registry_binding.rb +43 -0
  9. data/lib/smith/agent.rb +3 -4
  10. data/lib/smith/doctor/checks/models_registry.rb +25 -14
  11. data/lib/smith/doctor/checks/persistence_capabilities.rb +37 -29
  12. data/lib/smith/version.rb +1 -1
  13. data/lib/smith/workflow/agent_result.rb +26 -0
  14. data/lib/smith/workflow/branch_env.rb +24 -0
  15. data/lib/smith/workflow/budget_integration.rb +2 -0
  16. data/lib/smith/workflow/deterministic_execution.rb +1 -1
  17. data/lib/smith/workflow/deterministic_step.rb +32 -4
  18. data/lib/smith/workflow/durability.rb +38 -12
  19. data/lib/smith/workflow/evaluator_optimizer.rb +5 -8
  20. data/lib/smith/workflow/event_integration.rb +3 -3
  21. data/lib/smith/workflow/execution.rb +2 -0
  22. data/lib/smith/workflow/fanout_execution.rb +2 -0
  23. data/lib/smith/workflow/graph/contract_helpers.rb +65 -0
  24. data/lib/smith/workflow/graph/fanout_contract.rb +140 -0
  25. data/lib/smith/workflow/graph/nested_readiness_diagnostics.rb +98 -0
  26. data/lib/smith/workflow/graph/optimization_contract.rb +96 -0
  27. data/lib/smith/workflow/graph/orchestration_contract.rb +83 -0
  28. data/lib/smith/workflow/graph/runtime_binding_diagnostic_builder.rb +124 -0
  29. data/lib/smith/workflow/graph/runtime_binding_diagnostics.rb +114 -0
  30. data/lib/smith/workflow/graph/runtime_readiness.rb +63 -0
  31. data/lib/smith/workflow/graph/runtime_readiness_metrics.rb +87 -0
  32. data/lib/smith/workflow/graph/runtime_readiness_report.rb +65 -0
  33. data/lib/smith/workflow/graph/targets.rb +5 -0
  34. data/lib/smith/workflow/graph/transition_diagnostics.rb +7 -0
  35. data/lib/smith/workflow/graph/transition_snapshot.rb +61 -20
  36. data/lib/smith/workflow/graph.rb +16 -1
  37. data/lib/smith/workflow/graph_dsl.rb +4 -0
  38. data/lib/smith/workflow/guardrail_integration.rb +20 -3
  39. data/lib/smith/workflow/nested_execution.rb +5 -4
  40. data/lib/smith/workflow/optimization_state.rb +13 -0
  41. data/lib/smith/workflow/orchestration_state.rb +13 -0
  42. data/lib/smith/workflow/orchestrator_worker.rb +3 -15
  43. data/lib/smith/workflow/parallel/cancellation_signal.rb +21 -0
  44. data/lib/smith/workflow/parallel.rb +6 -15
  45. data/lib/smith/workflow/parallel_execution.rb +2 -0
  46. data/lib/smith/workflow/persistence.rb +37 -6
  47. data/lib/smith/workflow/router.rb +15 -4
  48. data/lib/smith/workflow/run_result.rb +54 -0
  49. data/lib/smith/workflow/transition.rb +90 -4
  50. data/lib/smith/workflow/usage_entry.rb +30 -0
  51. data/lib/smith/workflow/worker_execution.rb +13 -0
  52. data/lib/smith/workflow.rb +40 -136
  53. data/lib/smith.rb +9 -0
  54. metadata +21 -1
@@ -6,6 +6,11 @@ require "securerandom"
6
6
  require "set"
7
7
  require "time"
8
8
 
9
+ require_relative "workflow/agent_result"
10
+ require_relative "workflow/branch_env"
11
+ require_relative "workflow/run_result"
12
+ require_relative "workflow/usage_entry"
13
+
9
14
  module Smith
10
15
  class Workflow
11
16
  include DSL
@@ -21,113 +26,6 @@ module Smith
21
26
 
22
27
  DEFAULT_MAX_TRANSITIONS = 100
23
28
 
24
- # `keyword_init: true` is mandatory: `build_run_result` constructs
25
- # the result via keyword arguments. Plain Ruby Structs treat the
26
- # kwargs hash as the first positional field, silently leaving the
27
- # remaining fields nil — verified empirically. The `keyword_init`
28
- # flag routes kwargs to the right fields. `usage_entries` is the
29
- # 10th field, added in this slice for hadithi billing.
30
- RunResult = Struct.new(:state, :output, :steps, :total_cost, :total_tokens, :context, :session_messages,
31
- :tool_results, :outcome, :usage_entries, keyword_init: true) do
32
- def done?
33
- state == :done
34
- end
35
-
36
- def failed?
37
- state == :failed
38
- end
39
-
40
- def terminal_output
41
- output
42
- end
43
-
44
- def outcome_kind
45
- outcome&.dig(:kind)
46
- end
47
-
48
- def outcome_payload
49
- outcome&.dig(:payload)
50
- end
51
-
52
- def last_error
53
- steps.reverse.map { |step| step[:error] }.compact.first
54
- end
55
-
56
- def failed_transition
57
- failure_detail&.fetch(:transition)
58
- end
59
-
60
- def failure_detail
61
- failed_step = steps.reverse.find { |step| step[:error] }
62
- return nil unless failed_step
63
-
64
- {
65
- transition: failed_step[:transition],
66
- from: failed_step[:from],
67
- to: failed_step[:to],
68
- error: failed_step[:error]
69
- }
70
- end
71
- end
72
-
73
- # keyword_init: true so adding new fields in future schema versions
74
- # remains backward-compatible: `from_response` and `from_h` can fill
75
- # extra fields without breaking existing call sites that pass the
76
- # historical positional args. Reading old persisted records into a
77
- # newer Struct shape: from_h slices to current members (unknown keys
78
- # silently dropped; missing keys default to nil).
79
- AgentResult = Struct.new(
80
- :content, :input_tokens, :output_tokens, :cost, :model_used,
81
- keyword_init: true
82
- ) do
83
- def self.from_response(response, content, model_used: nil)
84
- new(
85
- content: content,
86
- input_tokens: response.respond_to?(:input_tokens) ? response.input_tokens : nil,
87
- output_tokens: response.respond_to?(:output_tokens) ? response.output_tokens : nil,
88
- cost: nil,
89
- model_used: model_used
90
- )
91
- end
92
-
93
- def usage_known?
94
- !input_tokens.nil? && !output_tokens.nil?
95
- end
96
- end
97
-
98
- # One row per agent provider call. `usage_id` is a UUID generated
99
- # at recording time and stable across persist/restore — hadithi
100
- # uses it as the idempotency anchor on `usage_events.smith_usage_id`.
101
- # Includes `to_h`/`from_h` for JSON serialization (plain Struct
102
- # JSON-encodes to `"#<struct ...>"` — useless).
103
- #
104
- # keyword_init: true gives forward/backward compatibility:
105
- # - Adding a new field: old persisted hashes restore cleanly (new
106
- # field defaults to nil).
107
- # - Reading new persisted hashes with an older Smith version: from_h
108
- # slices to known members (unknown keys silently dropped).
109
- UsageEntry = Struct.new(
110
- :usage_id,
111
- :agent_name,
112
- :model,
113
- :input_tokens,
114
- :output_tokens,
115
- :cost,
116
- :attempt_kind,
117
- :recorded_at,
118
- keyword_init: true
119
- ) do
120
- def self.from_h(hash)
121
- sym = hash.transform_keys(&:to_sym)
122
- filtered = sym.slice(*members)
123
- # Symbolize :agent_name and :attempt_kind for backward compat
124
- # with callers that consume the field as Symbol.
125
- filtered[:agent_name] = filtered[:agent_name].to_sym if filtered[:agent_name].is_a?(String)
126
- filtered[:attempt_kind] = filtered[:attempt_kind].to_sym if filtered[:attempt_kind].is_a?(String)
127
- new(**filtered)
128
- end
129
- end
130
-
131
29
  # Reconstruct Smith error classes from `@last_failed_step` snapshots.
132
30
  # Order matters: more-specific subclasses first, so a real DSF doesn't
133
31
  # get caught by the WorkflowError handler. Each lambda preserves the
@@ -167,24 +65,6 @@ module Smith
167
65
  RETRYABLE_BEARING_FAMILIES = %w[deterministic_step_failure tool_guardrail_failed].freeze
168
66
  private_constant :RETRYABLE_BEARING_FAMILIES
169
67
 
170
- # keyword_init: true for forward/backward compat (see UsageEntry).
171
- BranchEnv = Struct.new(
172
- :prepared_input, :guardrail_sources, :scoped_store, :branch_estimates, :deadline,
173
- keyword_init: true
174
- ) do
175
- def setup_thread
176
- Smith::Tool.current_guardrails = guardrail_sources
177
- Smith::Tool.current_deadline = deadline
178
- Smith.scoped_artifacts = scoped_store
179
- end
180
-
181
- def teardown_thread
182
- Smith::Tool.current_guardrails = nil
183
- Smith::Tool.current_deadline = nil
184
- Smith.scoped_artifacts = nil
185
- end
186
- end
187
-
188
68
  attr_reader :state, :last_prepared_input, :session_messages, :ledger
189
69
 
190
70
  def initialize(context: {}, ledger: nil, created_at: nil)
@@ -239,12 +119,13 @@ module Smith
239
119
  end
240
120
 
241
121
  def advance!
242
- max = self.class.max_transitions || DEFAULT_MAX_TRANSITIONS
243
- raise MaxTransitionsExceeded if @step_count >= max
122
+ ensure_transition_budget!
123
+ @step_work_started = false
244
124
 
245
125
  transition = resolve_transition
246
126
  return if transition.nil?
247
127
 
128
+ @step_work_started = true
248
129
  step_result = execute_step(transition)
249
130
  @step_count += 1
250
131
  @updated_at = Time.now.utc.iso8601
@@ -274,11 +155,11 @@ module Smith
274
155
  end
275
156
 
276
157
  def done?
277
- @state == :done
158
+ state_named?(:done)
278
159
  end
279
160
 
280
161
  def failed?
281
- @state == :failed
162
+ state_named?(:failed)
282
163
  end
283
164
 
284
165
  def record_persisted_key!(key)
@@ -360,17 +241,40 @@ module Smith
360
241
  true
361
242
  end
362
243
 
244
+ def ensure_transition_budget!
245
+ max = self.class.max_transitions || DEFAULT_MAX_TRANSITIONS
246
+ raise MaxTransitionsExceeded if @step_count >= max
247
+ end
248
+
363
249
  def resolve_transition
364
250
  if @next_transition_name
365
251
  name = @next_transition_name
366
252
  @next_transition_name = nil
367
- self.class.find_transition(name) ||
253
+ transition = self.class.find_transition(name) ||
368
254
  raise(UnresolvedTransitionError.new(name, self.class, @state))
255
+ validate_transition_origin!(transition)
256
+ transition
369
257
  else
370
258
  self.class.transitions_from(@state).first
371
259
  end
372
260
  end
373
261
 
262
+ def validate_transition_origin!(transition)
263
+ expected = transition.from
264
+ return if expected.nil? || expected == @state
265
+
266
+ raise WorkflowError,
267
+ "transition :#{transition.name} cannot run from state :#{@state}; expected :#{expected}"
268
+ end
269
+
270
+ def step_work_started?
271
+ @step_work_started == true
272
+ end
273
+
274
+ def state_named?(name)
275
+ @state == name || @state.to_s == name.to_s
276
+ end
277
+
374
278
  def build_run_result(steps)
375
279
  # `output` derivation matches existing semantics on fresh runs
376
280
  # (last non-nil step output via `compact.first`). Terminal-restore
@@ -416,8 +320,8 @@ module Smith
416
320
  # Skip const_get for retryable-bearing families. An unknown
417
321
  # subclass with a message-only constructor would const_get
418
322
  # successfully but discard the snapshot's `retryable`/`kind`/
419
- # `details` (defaults to nil), and hadithi's `retryable?`
420
- # check would misclassify a retryable failure as terminal.
323
+ # `details` (defaults to nil), and host retry classification
324
+ # could misclassify a retryable failure as terminal.
421
325
  # Family fallback rebuilds the parent class with kwargs intact.
422
326
  family_fallback(snap)
423
327
  else
@@ -436,9 +340,9 @@ module Smith
436
340
  # symbols; JSON round-trip stringifies them; coerce back to
437
341
  # match fresh-run shape exactly.
438
342
  {
439
- transition: snap[:transition]&.to_sym,
440
- from: snap[:from]&.to_sym,
441
- to: snap[:to]&.to_sym,
343
+ transition: normalize_transition_name(snap[:transition]),
344
+ from: normalize_state_name(snap[:from]),
345
+ to: normalize_state_name(snap[:to]),
442
346
  error: error
443
347
  }
444
348
  end
@@ -519,7 +423,7 @@ module Smith
519
423
  # `Struct#dup` is shallow — it shares mutable string fields between
520
424
  # the original and the duplicate. Smith's existing snapshot helpers
521
425
  # (`snapshot_context`, etc.) also use this round-trip pattern; the
522
- # billing-facing RunResult must not alias mutable workflow state.
426
+ # usage-facing RunResult must not alias mutable workflow state.
523
427
  # Same rule applies to nested-workflow rollup (see
524
428
  # `nested_execution.rb`).
525
429
  def snapshot_usage_entries
data/lib/smith.rb CHANGED
@@ -224,12 +224,20 @@ require_relative "smith/context/session"
224
224
  # Agent (depends on RubyLLM::Agent)
225
225
  require_relative "smith/agent"
226
226
  require_relative "smith/agent/lifecycle"
227
+ require_relative "smith/agent/registry_binding"
227
228
  require_relative "smith/agent/registry"
228
229
 
229
230
  # Workflow (Transition, DSL, Persistence, and Execution must load before Workflow)
230
231
  require_relative "smith/workflow/transition"
231
232
  require_relative "smith/workflow/graph"
232
233
  require_relative "smith/workflow/graph_dsl"
234
+ require_relative "smith/workflow/run_result"
235
+ require_relative "smith/workflow/agent_result"
236
+ require_relative "smith/workflow/usage_entry"
237
+ require_relative "smith/workflow/branch_env"
238
+ require_relative "smith/workflow/optimization_state"
239
+ require_relative "smith/workflow/orchestration_state"
240
+ require_relative "smith/workflow/worker_execution"
233
241
  require_relative "smith/workflow/dsl"
234
242
  require_relative "smith/workflow/persistence"
235
243
  require_relative "smith/workflow/durability"
@@ -254,6 +262,7 @@ require_relative "smith/workflow/execution_frame"
254
262
  require_relative "smith/workflow/pipeline"
255
263
  require_relative "smith/workflow/router"
256
264
  require_relative "smith/workflow/parallel"
265
+ require_relative "smith/workflow/parallel/cancellation_signal"
257
266
  require_relative "smith/workflow/parallel/cancellation"
258
267
 
259
268
  # Conditional Rails integration
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smith-agents
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.2
4
+ version: 0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Ralak
@@ -135,6 +135,8 @@ files:
135
135
  - lib/smith/agent.rb
136
136
  - lib/smith/agent/lifecycle.rb
137
137
  - lib/smith/agent/registry.rb
138
+ - lib/smith/agent/registry/introspection.rb
139
+ - lib/smith/agent/registry_binding.rb
138
140
  - lib/smith/artifacts.rb
139
141
  - lib/smith/artifacts/file.rb
140
142
  - lib/smith/artifacts/memory.rb
@@ -206,7 +208,9 @@ files:
206
208
  - lib/smith/types.rb
207
209
  - lib/smith/version.rb
208
210
  - lib/smith/workflow.rb
211
+ - lib/smith/workflow/agent_result.rb
209
212
  - lib/smith/workflow/artifact_integration.rb
213
+ - lib/smith/workflow/branch_env.rb
210
214
  - lib/smith/workflow/budget_integration.rb
211
215
  - lib/smith/workflow/claim.rb
212
216
  - lib/smith/workflow/data_volume_policy.rb
@@ -221,12 +225,22 @@ files:
221
225
  - lib/smith/workflow/execution_frame.rb
222
226
  - lib/smith/workflow/fanout_execution.rb
223
227
  - lib/smith/workflow/graph.rb
228
+ - lib/smith/workflow/graph/contract_helpers.rb
224
229
  - lib/smith/workflow/graph/diagnostic.rb
230
+ - lib/smith/workflow/graph/fanout_contract.rb
225
231
  - lib/smith/workflow/graph/metrics.rb
232
+ - lib/smith/workflow/graph/nested_readiness_diagnostics.rb
233
+ - lib/smith/workflow/graph/optimization_contract.rb
234
+ - lib/smith/workflow/graph/orchestration_contract.rb
226
235
  - lib/smith/workflow/graph/reachability.rb
227
236
  - lib/smith/workflow/graph/reachability_diagnostics.rb
228
237
  - lib/smith/workflow/graph/reference.rb
229
238
  - lib/smith/workflow/graph/report.rb
239
+ - lib/smith/workflow/graph/runtime_binding_diagnostic_builder.rb
240
+ - lib/smith/workflow/graph/runtime_binding_diagnostics.rb
241
+ - lib/smith/workflow/graph/runtime_readiness.rb
242
+ - lib/smith/workflow/graph/runtime_readiness_metrics.rb
243
+ - lib/smith/workflow/graph/runtime_readiness_report.rb
230
244
  - lib/smith/workflow/graph/state_diagnostics.rb
231
245
  - lib/smith/workflow/graph/targets.rb
232
246
  - lib/smith/workflow/graph/transition_diagnostics.rb
@@ -235,15 +249,21 @@ files:
235
249
  - lib/smith/workflow/graph_dsl.rb
236
250
  - lib/smith/workflow/guardrail_integration.rb
237
251
  - lib/smith/workflow/nested_execution.rb
252
+ - lib/smith/workflow/optimization_state.rb
253
+ - lib/smith/workflow/orchestration_state.rb
238
254
  - lib/smith/workflow/orchestrator_worker.rb
239
255
  - lib/smith/workflow/parallel.rb
240
256
  - lib/smith/workflow/parallel/cancellation.rb
257
+ - lib/smith/workflow/parallel/cancellation_signal.rb
241
258
  - lib/smith/workflow/parallel_execution.rb
242
259
  - lib/smith/workflow/persistence.rb
243
260
  - lib/smith/workflow/pipeline.rb
244
261
  - lib/smith/workflow/retry_execution.rb
245
262
  - lib/smith/workflow/router.rb
263
+ - lib/smith/workflow/run_result.rb
246
264
  - lib/smith/workflow/transition.rb
265
+ - lib/smith/workflow/usage_entry.rb
266
+ - lib/smith/workflow/worker_execution.rb
247
267
  - script/profile_tool_results.rb
248
268
  - sig/smith.rbs
249
269
  homepage: https://github.com/samuelralak/smith