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
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ class Pipeline
6
+ attr_reader :name, :from, :to, :stages, :failure_transition
7
+
8
+ def initialize(name, from:, to:, &)
9
+ raise WorkflowError, "pipeline name is required" if name.nil?
10
+ raise WorkflowError, "pipeline :#{name} requires from:" if from.nil?
11
+ raise WorkflowError, "pipeline :#{name} requires to:" if to.nil?
12
+
13
+ @name = name
14
+ @from = from
15
+ @to = to
16
+ @stages = []
17
+ @failure_transition = nil
18
+ instance_eval(&)
19
+ end
20
+
21
+ def stage(stage_name, execute:)
22
+ raise WorkflowError, "pipeline :#{name} stage name is required" if stage_name.nil?
23
+ raise WorkflowError, "pipeline :#{name} stage :#{stage_name} requires execute:" if execute.nil?
24
+
25
+ @stages << { name: stage_name, agent: execute }
26
+ end
27
+
28
+ def on_failure(transition_name)
29
+ @failure_transition = transition_name
30
+ end
31
+
32
+ def compile!(workflow_class)
33
+ validate!
34
+ validate_no_collisions!(workflow_class)
35
+ generate_transitions(workflow_class)
36
+ end
37
+
38
+ private
39
+
40
+ def validate!
41
+ raise WorkflowError, "pipeline :#{name} must declare at least one stage" if stages.empty?
42
+
43
+ seen = {}
44
+ stages.each do |stg|
45
+ raise WorkflowError, "pipeline :#{name} has duplicate stage :#{stg[:name]}" if seen[stg[:name]]
46
+
47
+ seen[stg[:name]] = true
48
+ end
49
+ end
50
+
51
+ def validate_no_collisions!(workflow_class)
52
+ stages.each do |stg|
53
+ t_name = stage_transition_name(stg)
54
+ next unless workflow_class.find_transition(t_name)
55
+
56
+ raise WorkflowError, "pipeline :#{name} transition :#{t_name} collides with existing transition"
57
+ end
58
+ end
59
+
60
+ def generate_transitions(workflow_class)
61
+ stages.each_with_index do |stg, idx|
62
+ declare_intermediate_state(workflow_class, idx)
63
+ register_stage_transition(workflow_class, stg, idx)
64
+ end
65
+ end
66
+
67
+ def declare_intermediate_state(workflow_class, idx)
68
+ return if idx.zero?
69
+
70
+ workflow_class.state(stage_after_state(stages[idx - 1]))
71
+ end
72
+
73
+ def register_stage_transition(workflow_class, stg, idx)
74
+ attrs = stage_attributes(stg, idx)
75
+
76
+ workflow_class.transition(attrs[:name], from: attrs[:from], to: attrs[:to]) do
77
+ execute attrs[:agent]
78
+ on_success attrs[:next] if attrs[:next]
79
+ on_failure attrs[:fail] if attrs[:fail]
80
+ end
81
+ end
82
+
83
+ def stage_attributes(stg, idx)
84
+ { name: stage_transition_name(stg), from: stage_from(idx), to: stage_to(stg, idx),
85
+ next: stage_next(idx), fail: failure_transition, agent: stg[:agent] }
86
+ end
87
+
88
+ def stage_from(idx)
89
+ idx.zero? ? from : stage_after_state(stages[idx - 1])
90
+ end
91
+
92
+ def stage_to(stg, idx)
93
+ idx == stages.length - 1 ? to : stage_after_state(stg)
94
+ end
95
+
96
+ def stage_next(idx)
97
+ idx < stages.length - 1 ? stage_transition_name(stages[idx + 1]) : nil
98
+ end
99
+
100
+ def stage_transition_name(stg)
101
+ :"#{name}__#{stg[:name]}"
102
+ end
103
+
104
+ def stage_after_state(stg)
105
+ :"#{name}__after_#{stg[:name]}"
106
+ end
107
+ end
108
+
109
+ module DSL
110
+ module ClassMethods
111
+ def pipeline(name, from:, to:, &)
112
+ Pipeline.new(name, from: from, to: to, &).compile!(self)
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ class Router
6
+ def self.resolve(classifier_output, config, workflow_class:)
7
+ validate!(classifier_output, config)
8
+ transition_name = select_transition(classifier_output, config)
9
+ validate_transition_exists!(transition_name, workflow_class)
10
+ transition_name
11
+ end
12
+
13
+ def self.validate!(output, config)
14
+ validate_structure!(output)
15
+ validate_confidence!(output[:confidence])
16
+ validate_route_key!(output[:route].to_sym, output[:confidence], config)
17
+ end
18
+
19
+ def self.validate_structure!(output)
20
+ raise WorkflowError, "router classifier output must be a Hash" unless output.is_a?(Hash)
21
+ raise WorkflowError, "router classifier output missing :route" unless output.key?(:route)
22
+ raise WorkflowError, "router classifier output missing :confidence" unless output.key?(:confidence)
23
+ end
24
+
25
+ def self.validate_confidence!(confidence)
26
+ return if confidence.is_a?(Numeric) && confidence >= 0.0 && confidence <= 1.0
27
+
28
+ raise WorkflowError, "router confidence must be a number in 0.0..1.0"
29
+ end
30
+
31
+ def self.validate_route_key!(route_key, confidence, config)
32
+ return if confidence < config[:confidence_threshold]
33
+ return if config[:routes].key?(route_key)
34
+
35
+ raise WorkflowError, "router route :#{route_key} not found in declared routes"
36
+ end
37
+
38
+ def self.select_transition(output, config)
39
+ if output[:confidence] >= config[:confidence_threshold]
40
+ config[:routes][output[:route].to_sym]
41
+ else
42
+ config[:fallback]
43
+ end
44
+ end
45
+
46
+ def self.validate_transition_exists!(transition_name, workflow_class)
47
+ return if workflow_class.find_transition(transition_name)
48
+
49
+ raise WorkflowError, "router selected transition :#{transition_name} which is not declared on the workflow"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smith
4
+ class Workflow
5
+ class Transition
6
+ attr_reader :name, :from, :to, :agent_name, :agent_opts, :success_transition, :failure_transition,
7
+ :router_config, :workflow_class, :optimization_config, :orchestrator_config,
8
+ :deterministic_block, :deterministic_kind
9
+
10
+ def initialize(name, from:, to:, &)
11
+ @name = name
12
+ @from = from
13
+ @to = to
14
+ instance_eval(&) if block_given?
15
+ end
16
+
17
+ def execute(agent_name, **opts)
18
+ raise WorkflowError, "transition cannot declare both execute and compute/run" if @deterministic_block
19
+
20
+ @agent_name = agent_name
21
+ @agent_opts = opts
22
+ end
23
+
24
+ def on_success(transition_name)
25
+ @success_transition = transition_name
26
+ end
27
+
28
+ def on_failure(transition_name)
29
+ @failure_transition = transition_name
30
+ end
31
+
32
+ def route(agent_name, routes:, confidence_threshold:, fallback:)
33
+ raise WorkflowError, "transition cannot declare both route and compute/run" if @deterministic_block
34
+
35
+ @agent_name = agent_name
36
+ @router_config = { routes: routes, confidence_threshold: confidence_threshold, fallback: fallback }
37
+ end
38
+
39
+ def workflow(klass)
40
+ raise WorkflowError, "workflow binding must be a Class" unless klass.is_a?(Class)
41
+ raise WorkflowError, "workflow binding must be a Smith::Workflow subclass" unless klass < Workflow
42
+ raise WorkflowError, "transition cannot declare both workflow and execute" if @agent_name && !@router_config
43
+ raise WorkflowError, "transition cannot declare both workflow and route" if @router_config
44
+ raise WorkflowError, "transition cannot declare both workflow and compute/run" if @deterministic_block
45
+
46
+ @workflow_class = klass
47
+ end
48
+
49
+ def optimize(generator:, evaluator:, max_rounds:, evaluator_schema:,
50
+ improvement_threshold: nil,
51
+ evaluator_context: nil,
52
+ before_eval: nil,
53
+ on_exhaustion: :raise,
54
+ on_converged: :raise,
55
+ on_threshold: :raise)
56
+ validate_optimize_conflicts!
57
+ validate_optimize_controls!(generator, evaluator, max_rounds, evaluator_schema)
58
+ validate_optimize_exit_modes!(on_exhaustion: on_exhaustion, on_converged: on_converged,
59
+ on_threshold: on_threshold)
60
+ validate_optimize_evaluator_context!(evaluator_context)
61
+ validate_optimize_before_eval!(before_eval)
62
+
63
+ @optimization_config = {
64
+ generator: generator, evaluator: evaluator, max_rounds: max_rounds,
65
+ evaluator_schema: evaluator_schema, improvement_threshold: improvement_threshold,
66
+ evaluator_context: evaluator_context,
67
+ before_eval: before_eval,
68
+ on_exhaustion: on_exhaustion,
69
+ on_converged: on_converged,
70
+ on_threshold: on_threshold
71
+ }
72
+ end
73
+
74
+ def orchestrate(**opts)
75
+ validate_orchestrate_conflicts!
76
+ validate_orchestrate_controls!(opts)
77
+ @orchestrator_config = opts
78
+ end
79
+
80
+ %i[compute run].each do |method_name|
81
+ define_method(method_name) do |&block|
82
+ validate_deterministic_conflicts!
83
+ raise WorkflowError, "#{method_name} requires a block" unless block
84
+
85
+ @deterministic_block = block
86
+ @deterministic_kind = method_name
87
+ end
88
+ end
89
+
90
+ def deterministic?
91
+ !@deterministic_block.nil?
92
+ end
93
+
94
+ def orchestrated?
95
+ !@orchestrator_config.nil?
96
+ end
97
+
98
+ def optimized?
99
+ !@optimization_config.nil?
100
+ end
101
+
102
+ def nested?
103
+ !@workflow_class.nil?
104
+ end
105
+
106
+ def routed?
107
+ !@router_config.nil?
108
+ end
109
+
110
+ def parallel?
111
+ agent_opts&.dig(:parallel) == true
112
+ end
113
+
114
+ private
115
+
116
+ def validate_deterministic_conflicts!
117
+ raise WorkflowError, "transition cannot declare both compute/run and execute" if @agent_name && !@router_config
118
+ raise WorkflowError, "transition cannot declare both compute/run and route" if @router_config
119
+ raise WorkflowError, "transition cannot declare both compute/run and workflow" if @workflow_class
120
+ raise WorkflowError, "transition cannot declare both compute/run and optimize" if @optimization_config
121
+ raise WorkflowError, "transition cannot declare both compute/run and orchestrate" if @orchestrator_config
122
+ raise WorkflowError, "transition cannot declare both compute and run" if @deterministic_block
123
+ end
124
+
125
+ def validate_optimize_conflicts!
126
+ raise WorkflowError, "transition cannot declare both optimize and execute" if @agent_name && !@router_config
127
+ raise WorkflowError, "transition cannot declare both optimize and route" if @router_config
128
+ raise WorkflowError, "transition cannot declare both optimize and workflow" if @workflow_class
129
+ raise WorkflowError, "transition cannot declare both optimize and compute/run" if @deterministic_block
130
+ end
131
+
132
+ def validate_orchestrate_conflicts!
133
+ raise WorkflowError, "transition cannot declare both orchestrate and execute" if @agent_name && !@router_config
134
+ raise WorkflowError, "transition cannot declare both orchestrate and route" if @router_config
135
+ raise WorkflowError, "transition cannot declare both orchestrate and workflow" if @workflow_class
136
+ raise WorkflowError, "transition cannot declare both orchestrate and optimize" if @optimization_config
137
+ raise WorkflowError, "transition cannot declare both orchestrate and compute/run" if @deterministic_block
138
+ end
139
+
140
+ def validate_orchestrate_controls!(opts)
141
+ validate_orchestrate_required_fields!(opts)
142
+ validate_orchestrate_bounds!(opts)
143
+ end
144
+
145
+ def validate_orchestrate_required_fields!(opts)
146
+ raise WorkflowError, "orchestrate requires an orchestrator" if opts[:orchestrator].nil?
147
+ raise WorkflowError, "orchestrate requires a worker" if opts[:worker].nil?
148
+
149
+ validate_schema_surface!(:task_schema, opts[:task_schema])
150
+ validate_schema_surface!(:worker_output_schema, opts[:worker_output_schema])
151
+ validate_schema_surface!(:final_output_schema, opts[:final_output_schema])
152
+ end
153
+
154
+ def validate_schema_surface!(name, schema)
155
+ raise WorkflowError, "orchestrate requires a #{name}" if schema.nil?
156
+ return if schema.respond_to?(:required_keys)
157
+
158
+ raise WorkflowError, "orchestrate #{name} must respond to :required_keys"
159
+ end
160
+
161
+ def validate_orchestrate_bounds!(opts)
162
+ unless opts[:max_workers].is_a?(Integer) && opts[:max_workers].positive?
163
+ raise WorkflowError, "orchestrate max_workers must be a positive integer"
164
+ end
165
+ return if opts[:max_delegation_rounds].is_a?(Integer) && opts[:max_delegation_rounds].positive?
166
+
167
+ raise WorkflowError, "orchestrate max_delegation_rounds must be a positive integer"
168
+ end
169
+
170
+ def validate_optimize_controls!(generator, evaluator, max_rounds, evaluator_schema)
171
+ raise WorkflowError, "optimize requires a generator" if generator.nil?
172
+ raise WorkflowError, "optimize requires an evaluator" if evaluator.nil?
173
+ raise WorkflowError, "optimize requires an evaluator_schema" if evaluator_schema.nil?
174
+
175
+ return if max_rounds.is_a?(Integer) && max_rounds.positive?
176
+
177
+ raise WorkflowError, "optimize max_rounds must be a positive integer"
178
+ end
179
+
180
+ VALID_EXIT_MODES = [:raise, :return_last].freeze
181
+ private_constant :VALID_EXIT_MODES
182
+
183
+ def validate_optimize_exit_modes!(on_exhaustion:, on_converged:, on_threshold:)
184
+ { on_exhaustion: on_exhaustion, on_converged: on_converged, on_threshold: on_threshold }.each do |name, value|
185
+ next if VALID_EXIT_MODES.include?(value)
186
+ next if value.respond_to?(:call)
187
+
188
+ raise WorkflowError,
189
+ "optimize #{name} must be :raise, :return_last, or a callable; got #{value.inspect}"
190
+ end
191
+ end
192
+
193
+ def validate_optimize_evaluator_context!(evaluator_context)
194
+ return if evaluator_context.nil? || evaluator_context == :inject_state
195
+
196
+ raise WorkflowError,
197
+ "optimize evaluator_context must be nil or :inject_state; got #{evaluator_context.inspect}"
198
+ end
199
+
200
+ def validate_optimize_before_eval!(before_eval)
201
+ return if before_eval.nil?
202
+ return if before_eval.respond_to?(:call)
203
+
204
+ raise WorkflowError, "optimize before_eval must respond to :call; got #{before_eval.inspect}"
205
+ end
206
+ end
207
+ end
208
+ end