igniter 0.4.5 → 0.5.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 (154) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +217 -0
  3. data/docs/APPLICATION_V1.md +253 -0
  4. data/docs/CAPABILITIES_V1.md +207 -0
  5. data/docs/CONSENSUS_V1.md +477 -0
  6. data/docs/CONTENT_ADDRESSING_V1.md +221 -0
  7. data/docs/DATAFLOW_V1.md +274 -0
  8. data/docs/MESH_V1.md +732 -0
  9. data/docs/NODE_CACHE_V1.md +324 -0
  10. data/docs/PROACTIVE_AGENTS_V1.md +293 -0
  11. data/docs/SERVER_V1.md +200 -1
  12. data/docs/SKILLS_V1.md +213 -0
  13. data/docs/STORE_ADAPTERS.md +41 -13
  14. data/docs/TEMPORAL_V1.md +174 -0
  15. data/docs/TOOLS_V1.md +347 -0
  16. data/docs/TRANSCRIPTION_V1.md +403 -0
  17. data/examples/README.md +37 -0
  18. data/examples/consensus.rb +239 -0
  19. data/examples/dataflow.rb +308 -0
  20. data/examples/elocal_webhook.rb +1 -0
  21. data/examples/llm_tools.rb +237 -0
  22. data/examples/mesh.rb +239 -0
  23. data/examples/mesh_discovery.rb +267 -0
  24. data/examples/mesh_gossip.rb +162 -0
  25. data/examples/ringcentral_routing.rb +1 -1
  26. data/lib/igniter/agents/ai/alert_agent.rb +111 -0
  27. data/lib/igniter/agents/ai/chain_agent.rb +127 -0
  28. data/lib/igniter/agents/ai/critic_agent.rb +163 -0
  29. data/lib/igniter/agents/ai/evaluator_agent.rb +193 -0
  30. data/lib/igniter/agents/ai/evolution_agent.rb +286 -0
  31. data/lib/igniter/agents/ai/health_check_agent.rb +122 -0
  32. data/lib/igniter/agents/ai/observer_agent.rb +184 -0
  33. data/lib/igniter/agents/ai/planner_agent.rb +210 -0
  34. data/lib/igniter/agents/ai/router_agent.rb +131 -0
  35. data/lib/igniter/agents/ai/self_reflection_agent.rb +175 -0
  36. data/lib/igniter/agents/observability/metrics_agent.rb +130 -0
  37. data/lib/igniter/agents/pipeline/batch_processor_agent.rb +131 -0
  38. data/lib/igniter/agents/proactive_agent.rb +208 -0
  39. data/lib/igniter/agents/reliability/retry_agent.rb +99 -0
  40. data/lib/igniter/agents/scheduling/cron_agent.rb +110 -0
  41. data/lib/igniter/agents.rb +56 -0
  42. data/lib/igniter/application/app_config.rb +32 -0
  43. data/lib/igniter/application/autoloader.rb +18 -0
  44. data/lib/igniter/application/generator.rb +157 -0
  45. data/lib/igniter/application/scheduler.rb +109 -0
  46. data/lib/igniter/application/yml_loader.rb +39 -0
  47. data/lib/igniter/application.rb +174 -0
  48. data/lib/igniter/capabilities.rb +68 -0
  49. data/lib/igniter/compiler/validators/dependencies_validator.rb +50 -2
  50. data/lib/igniter/compiler/validators/remote_validator.rb +2 -0
  51. data/lib/igniter/consensus/cluster.rb +183 -0
  52. data/lib/igniter/consensus/errors.rb +14 -0
  53. data/lib/igniter/consensus/executors.rb +43 -0
  54. data/lib/igniter/consensus/node.rb +320 -0
  55. data/lib/igniter/consensus/read_query.rb +30 -0
  56. data/lib/igniter/consensus/state_machine.rb +58 -0
  57. data/lib/igniter/consensus.rb +58 -0
  58. data/lib/igniter/content_addressing.rb +133 -0
  59. data/lib/igniter/contract.rb +12 -0
  60. data/lib/igniter/dataflow/aggregate_operators.rb +147 -0
  61. data/lib/igniter/dataflow/aggregate_state.rb +77 -0
  62. data/lib/igniter/dataflow/diff.rb +37 -0
  63. data/lib/igniter/dataflow/diff_state.rb +81 -0
  64. data/lib/igniter/dataflow/incremental_collection_result.rb +39 -0
  65. data/lib/igniter/dataflow/window_filter.rb +48 -0
  66. data/lib/igniter/dataflow.rb +65 -0
  67. data/lib/igniter/dsl/contract_builder.rb +71 -7
  68. data/lib/igniter/executor.rb +60 -0
  69. data/lib/igniter/extensions/capabilities.rb +39 -0
  70. data/lib/igniter/extensions/content_addressing.rb +5 -0
  71. data/lib/igniter/extensions/dataflow.rb +117 -0
  72. data/lib/igniter/extensions/mesh.rb +31 -0
  73. data/lib/igniter/fingerprint.rb +43 -0
  74. data/lib/igniter/integrations/llm/config.rb +48 -4
  75. data/lib/igniter/integrations/llm/executor.rb +221 -28
  76. data/lib/igniter/integrations/llm/providers/anthropic.rb +37 -4
  77. data/lib/igniter/integrations/llm/providers/openai.rb +34 -5
  78. data/lib/igniter/integrations/llm/transcription/providers/assemblyai.rb +200 -0
  79. data/lib/igniter/integrations/llm/transcription/providers/base.rb +122 -0
  80. data/lib/igniter/integrations/llm/transcription/providers/deepgram.rb +162 -0
  81. data/lib/igniter/integrations/llm/transcription/providers/openai.rb +102 -0
  82. data/lib/igniter/integrations/llm/transcription/transcriber.rb +145 -0
  83. data/lib/igniter/integrations/llm/transcription/transcript_result.rb +29 -0
  84. data/lib/igniter/integrations/llm.rb +37 -1
  85. data/lib/igniter/memory/agent_memory.rb +104 -0
  86. data/lib/igniter/memory/episode.rb +29 -0
  87. data/lib/igniter/memory/fact.rb +27 -0
  88. data/lib/igniter/memory/memorable.rb +90 -0
  89. data/lib/igniter/memory/reflection_cycle.rb +96 -0
  90. data/lib/igniter/memory/reflection_record.rb +28 -0
  91. data/lib/igniter/memory/store.rb +115 -0
  92. data/lib/igniter/memory/stores/in_memory.rb +136 -0
  93. data/lib/igniter/memory/stores/sqlite.rb +284 -0
  94. data/lib/igniter/memory.rb +80 -0
  95. data/lib/igniter/mesh/announcer.rb +55 -0
  96. data/lib/igniter/mesh/config.rb +45 -0
  97. data/lib/igniter/mesh/discovery.rb +39 -0
  98. data/lib/igniter/mesh/errors.rb +31 -0
  99. data/lib/igniter/mesh/gossip.rb +47 -0
  100. data/lib/igniter/mesh/peer.rb +21 -0
  101. data/lib/igniter/mesh/peer_registry.rb +51 -0
  102. data/lib/igniter/mesh/poller.rb +77 -0
  103. data/lib/igniter/mesh/router.rb +109 -0
  104. data/lib/igniter/mesh.rb +85 -0
  105. data/lib/igniter/metrics/collector.rb +131 -0
  106. data/lib/igniter/metrics/prometheus_exporter.rb +104 -0
  107. data/lib/igniter/metrics/snapshot.rb +8 -0
  108. data/lib/igniter/metrics.rb +37 -0
  109. data/lib/igniter/model/aggregate_node.rb +34 -0
  110. data/lib/igniter/model/collection_node.rb +3 -2
  111. data/lib/igniter/model/compute_node.rb +13 -0
  112. data/lib/igniter/model/remote_node.rb +18 -2
  113. data/lib/igniter/node_cache.rb +231 -0
  114. data/lib/igniter/replication/bootstrapper.rb +61 -0
  115. data/lib/igniter/replication/bootstrappers/gem.rb +32 -0
  116. data/lib/igniter/replication/bootstrappers/git.rb +39 -0
  117. data/lib/igniter/replication/bootstrappers/tarball.rb +56 -0
  118. data/lib/igniter/replication/expansion_plan.rb +38 -0
  119. data/lib/igniter/replication/expansion_planner.rb +142 -0
  120. data/lib/igniter/replication/manifest.rb +45 -0
  121. data/lib/igniter/replication/network_topology.rb +123 -0
  122. data/lib/igniter/replication/node_role.rb +42 -0
  123. data/lib/igniter/replication/reflective_replication_agent.rb +238 -0
  124. data/lib/igniter/replication/replication_agent.rb +87 -0
  125. data/lib/igniter/replication/role_registry.rb +73 -0
  126. data/lib/igniter/replication/ssh_session.rb +77 -0
  127. data/lib/igniter/replication.rb +54 -0
  128. data/lib/igniter/runtime/execution.rb +18 -0
  129. data/lib/igniter/runtime/input_validator.rb +6 -2
  130. data/lib/igniter/runtime/resolver.rb +254 -16
  131. data/lib/igniter/runtime/stores/redis_store.rb +41 -4
  132. data/lib/igniter/server/client.rb +44 -1
  133. data/lib/igniter/server/config.rb +13 -6
  134. data/lib/igniter/server/handlers/event_handler.rb +4 -0
  135. data/lib/igniter/server/handlers/execute_handler.rb +6 -0
  136. data/lib/igniter/server/handlers/liveness_handler.rb +20 -0
  137. data/lib/igniter/server/handlers/manifest_handler.rb +34 -0
  138. data/lib/igniter/server/handlers/metrics_handler.rb +51 -0
  139. data/lib/igniter/server/handlers/peers_handler.rb +115 -0
  140. data/lib/igniter/server/handlers/readiness_handler.rb +47 -0
  141. data/lib/igniter/server/http_server.rb +54 -17
  142. data/lib/igniter/server/router.rb +54 -21
  143. data/lib/igniter/server/server_logger.rb +52 -0
  144. data/lib/igniter/server.rb +6 -0
  145. data/lib/igniter/skill/feedback.rb +116 -0
  146. data/lib/igniter/skill/output_schema.rb +110 -0
  147. data/lib/igniter/skill.rb +218 -0
  148. data/lib/igniter/temporal.rb +84 -0
  149. data/lib/igniter/tool/discoverable.rb +151 -0
  150. data/lib/igniter/tool.rb +52 -0
  151. data/lib/igniter/tool_registry.rb +144 -0
  152. data/lib/igniter/version.rb +1 -1
  153. data/lib/igniter.rb +17 -0
  154. metadata +122 -1
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Agents
5
+ # Processes items in configurable batches with error tracking.
6
+ #
7
+ # Items are enqueued via :enqueue and processed synchronously via
8
+ # :process_next (one batch) or :drain (all remaining items).
9
+ # Failed items are tracked with their errors for inspection.
10
+ #
11
+ # @example
12
+ # processor = ->(item:) { DataStore.upsert(item) }
13
+ #
14
+ # ref = BatchProcessorAgent.start(initial_state: { batch_size: 50 })
15
+ # ref.send(:enqueue, items: records, callable: processor)
16
+ # ref.send(:drain)
17
+ #
18
+ # status = ref.call(:status)
19
+ # puts "processed=#{status.processed} failed=#{status.failed}"
20
+ class BatchProcessorAgent < Igniter::Agent
21
+ # Returned by the sync :status query.
22
+ Status = Struct.new(:queue_size, :processed, :failed, keyword_init: true)
23
+
24
+ initial_state queue: [], processed: 0, failed: 0, errors: [], batch_size: 10
25
+
26
+ # Add items to the processing queue.
27
+ #
28
+ # Payload keys:
29
+ # items [Array] — required; items to process
30
+ # callable [#call] — receives (item:); required unless set via :configure
31
+ on :enqueue do |state:, payload:|
32
+ items = Array(payload.fetch(:items))
33
+ callable = payload[:callable] || state[:callable]
34
+ raise ArgumentError, ":callable required" unless callable
35
+
36
+ jobs = items.map { |item| { item: item, callable: callable } }
37
+ state.merge(queue: state[:queue] + jobs)
38
+ end
39
+
40
+ # Process the next batch_size items.
41
+ #
42
+ # Payload keys:
43
+ # batch_size [Integer] — override class default (optional)
44
+ on :process_next do |state:, payload:|
45
+ size = payload.fetch(:batch_size, state[:batch_size])
46
+ agent = new
47
+ agent.send(:run_batch, state, size)
48
+ end
49
+
50
+ # Process all remaining items synchronously (blocks until queue is empty).
51
+ on :drain do |state:, payload:|
52
+ size = payload.fetch(:batch_size, state[:batch_size])
53
+ agent = new
54
+ agent.send(:run_all, state, size)
55
+ end
56
+
57
+ # Sync status query.
58
+ #
59
+ # @return [Status]
60
+ on :status do |state:, **|
61
+ Status.new(
62
+ queue_size: state[:queue].size,
63
+ processed: state[:processed],
64
+ failed: state[:failed]
65
+ )
66
+ end
67
+
68
+ # Return error log for failed items.
69
+ #
70
+ # @return [Array<Hash>]
71
+ on :errors do |state:, **|
72
+ state[:errors]
73
+ end
74
+
75
+ # Reset counters and clear errors (queue is preserved).
76
+ on :reset_stats do |state:, **|
77
+ state.merge(processed: 0, failed: 0, errors: [])
78
+ end
79
+
80
+ # Set default batch_size and/or default callable.
81
+ #
82
+ # Payload keys:
83
+ # batch_size [Integer] — new default
84
+ # callable [#call] — new default callable
85
+ on :configure do |state:, payload:|
86
+ state.merge(
87
+ batch_size: payload.fetch(:batch_size, state[:batch_size]),
88
+ callable: payload.fetch(:callable, state[:callable])
89
+ )
90
+ end
91
+
92
+ private
93
+
94
+ def run_batch(state, size)
95
+ batch = state[:queue].first(size)
96
+ remaining = state[:queue].drop(size)
97
+ result = process_jobs(batch)
98
+ apply_result(state, remaining, result)
99
+ end
100
+
101
+ def run_all(state, size)
102
+ current = state
103
+ current = run_batch(current, size) until current[:queue].empty?
104
+ current
105
+ end
106
+
107
+ def process_jobs(batch)
108
+ processed = 0
109
+ failed = 0
110
+ errors = []
111
+ batch.each do |job|
112
+ job[:callable].call(item: job[:item])
113
+ processed += 1
114
+ rescue StandardError => e
115
+ failed += 1
116
+ errors << { item: job[:item], error: e.message }
117
+ end
118
+ { processed: processed, failed: failed, errors: errors }
119
+ end
120
+
121
+ def apply_result(state, remaining, result)
122
+ state.merge(
123
+ queue: remaining,
124
+ processed: state[:processed] + result[:processed],
125
+ failed: state[:failed] + result[:failed],
126
+ errors: state[:errors] + result[:errors]
127
+ )
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../integrations/agents"
4
+
5
+ module Igniter
6
+ module Agents
7
+ # Base class for proactive (self-initiating) agents.
8
+ #
9
+ # A reactive agent waits for messages. A *proactive* agent acts without
10
+ # being asked — it polls conditions on a schedule, evaluates triggers, and
11
+ # fires actions when conditions are met.
12
+ #
13
+ # == Design
14
+ #
15
+ # ProactiveAgent extends the standard Agent DSL with four new keywords:
16
+ #
17
+ # intent "Human-readable description of the agent's mission"
18
+ # scan_interval 5.0 # seconds between automatic scans
19
+ # watch :metric, poll: -> # callable returning a current reading
20
+ # trigger :name, # rule: condition + action
21
+ # condition: ->(ctx) { ... },
22
+ # action: ->(state:, context:) { ... }
23
+ #
24
+ # == Execution model
25
+ #
26
+ # Every +scan_interval+ seconds the agent's timer fires +:_scan+, which:
27
+ # 1. Calls each registered watcher to build a +context+ snapshot.
28
+ # 2. Evaluates every trigger's +condition+ against the context.
29
+ # 3. Calls the +action+ of every condition that returns truthy.
30
+ # 4. Merges context, scan count, and fired-trigger history into state.
31
+ #
32
+ # +:_scan+ can also be invoked programmatically (useful in specs):
33
+ # described_class.handlers[:_scan].call(state: state, payload: {})
34
+ #
35
+ # == Built-in message handlers (injected into every subclass)
36
+ #
37
+ # :_scan — run one scan cycle
38
+ # :pause — suspend automatic reactions (scans still run)
39
+ # :resume — resume reactions
40
+ # :status — sync query → Status struct
41
+ # :context — sync query → last context snapshot Hash
42
+ # :trigger_history — sync query → Array<FiredTrigger>
43
+ #
44
+ # == Initial state
45
+ #
46
+ # Call +proactive_initial_state+ instead of +initial_state+ to include
47
+ # the required ProactiveAgent keys while also adding your own:
48
+ #
49
+ # proactive_initial_state queue: [], threshold: 0.9
50
+ #
51
+ # == Example
52
+ #
53
+ # class ErrorRateMonitor < Igniter::Agents::ProactiveAgent
54
+ # intent "Alert when error rate exceeds 5%"
55
+ # scan_interval 10.0
56
+ #
57
+ # watch :error_rate, poll: -> { ErrorMetrics.current_rate }
58
+ #
59
+ # trigger :high_errors,
60
+ # condition: ->(ctx) { ctx[:error_rate].to_f > 0.05 },
61
+ # action: ->(state:, context:) {
62
+ # Notifier.alert("Error rate: #{context[:error_rate]}")
63
+ # state.merge(last_alert_at: Time.now)
64
+ # }
65
+ #
66
+ # proactive_initial_state last_alert_at: nil
67
+ # end
68
+ #
69
+ # ref = ErrorRateMonitor.start
70
+ # ref.call(:status) # => Status(active: true, scan_count: 0, ...)
71
+ #
72
+ class ProactiveAgent < Igniter::Agent
73
+ # Recorded when a trigger fires during a scan cycle.
74
+ FiredTrigger = Struct.new(:name, :fired_at, :context, keyword_init: true)
75
+
76
+ # Returned by the +:status+ sync query.
77
+ Status = Struct.new(:active, :scan_count, :intent,
78
+ :watchers, :triggers, :last_scan_at,
79
+ keyword_init: true)
80
+
81
+ class << self
82
+ # ── DSL ─────────────────────────────────────────────────────────────
83
+
84
+ # Declare the agent's human-readable mission (metadata only).
85
+ def intent(desc = nil)
86
+ return @intent if desc.nil?
87
+
88
+ @intent = desc
89
+ end
90
+
91
+ # Set the scan interval in seconds and register the recurring timer.
92
+ # The timer delegates to the +:_scan+ message handler so both the
93
+ # production path and specs share identical logic.
94
+ def scan_interval(seconds)
95
+ klass = self
96
+ schedule(:_scan, every: seconds.to_f) do |state:|
97
+ h = klass.handlers[:_scan]
98
+ h ? h.call(state: state, payload: {}) : nil
99
+ end
100
+ end
101
+
102
+ # Register a watcher: a named, zero-argument callable that returns a
103
+ # current reading of some value. Called at the start of every scan.
104
+ #
105
+ # @param name [Symbol]
106
+ # @param poll [#call] — should never raise (errors are rescued to nil)
107
+ def watch(name, poll:)
108
+ (@watchers ||= {})[name.to_sym] = poll
109
+ end
110
+
111
+ # Register a trigger: evaluated on every scan cycle.
112
+ # +condition+ receives the context Hash; +action+ receives
113
+ # +state:+ and +context:+ and must return a new state Hash or nil.
114
+ #
115
+ # @param name [Symbol]
116
+ # @param condition [#call] — (ctx) → truthy/falsy
117
+ # @param action [#call] — (state:, context:) → Hash | nil
118
+ def trigger(name, condition:, action:)
119
+ (@proactive_triggers ||= {})[name.to_sym] = {
120
+ condition: condition,
121
+ action: action
122
+ }
123
+ end
124
+
125
+ # Read accessors (safe even before any DSL calls).
126
+ def watchers = @watchers || {}
127
+ def proactive_triggers = @proactive_triggers || {}
128
+
129
+ # Convenience: set the initial state including the ProactiveAgent keys.
130
+ # Use instead of +initial_state+ to avoid forgetting required keys.
131
+ #
132
+ # @param extra [Hash] additional subclass-specific keys
133
+ def proactive_initial_state(extra = {})
134
+ initial_state({
135
+ active: true,
136
+ context: {},
137
+ scan_count: 0,
138
+ last_scan_at: nil,
139
+ trigger_history: []
140
+ }.merge(extra))
141
+ end
142
+
143
+ # ── Inheritance ──────────────────────────────────────────────────────
144
+
145
+ def inherited(subclass)
146
+ super # Agent.inherited: resets @handlers, @timers, @default_state, …
147
+ inject_proactive_handlers!(subclass)
148
+ end
149
+
150
+ private
151
+
152
+ # Inject all ProactiveAgent-level handlers into +klass+.
153
+ # Uses a captured +klass+ variable so the Procs access the correct
154
+ # subclass even though their lexical +self+ is ProactiveAgent.
155
+ def inject_proactive_handlers!(klass) # rubocop:disable Metrics/MethodLength
156
+ # ── :_scan ────────────────────────────────────────────────────────
157
+ klass.on(:_scan) do |state:, **|
158
+ next state unless state.fetch(:active, true)
159
+
160
+ ctx = klass.watchers.transform_values do |poll|
161
+ poll.call
162
+ rescue StandardError
163
+ nil
164
+ end
165
+
166
+ fired = []
167
+ new_state = klass.proactive_triggers.reduce(state) do |s, (name, t)|
168
+ next s unless t[:condition].call(ctx)
169
+
170
+ fired << FiredTrigger.new(name: name, fired_at: Time.now, context: ctx)
171
+ result = t[:action].call(state: s, context: ctx)
172
+ result.is_a?(Hash) ? result : s
173
+ end
174
+
175
+ new_state.merge(
176
+ context: ctx,
177
+ scan_count: new_state.fetch(:scan_count, 0) + 1,
178
+ last_scan_at: Time.now,
179
+ trigger_history: (new_state.fetch(:trigger_history, []) + fired).last(100)
180
+ )
181
+ end
182
+
183
+ # ── :pause / :resume ──────────────────────────────────────────────
184
+ klass.on(:pause) { |state:, **| state.merge(active: false) }
185
+ klass.on(:resume) { |state:, **| state.merge(active: true) }
186
+
187
+ # ── :status ───────────────────────────────────────────────────────
188
+ klass.on(:status) do |state:, **|
189
+ Status.new(
190
+ active: state.fetch(:active, true),
191
+ scan_count: state.fetch(:scan_count, 0),
192
+ intent: klass.intent,
193
+ watchers: klass.watchers.keys,
194
+ triggers: klass.proactive_triggers.keys,
195
+ last_scan_at: state[:last_scan_at]
196
+ )
197
+ end
198
+
199
+ # ── :context ─────────────────────────────────────────────────────
200
+ klass.on(:context) { |state:, **| state.fetch(:context, {}).dup }
201
+
202
+ # ── :trigger_history ─────────────────────────────────────────────
203
+ klass.on(:trigger_history) { |state:, **| state.fetch(:trigger_history, []).dup }
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Agents
5
+ # Executes a callable with automatic retry on failure.
6
+ #
7
+ # Supports three backoff strategies:
8
+ # * :immediate — retry without delay (useful in tests)
9
+ # * :linear — delay grows linearly: base_delay × attempt
10
+ # * :exponential — delay doubles each time: base_delay × 2^(attempt-1)
11
+ # Add jitter: true to randomise delay ±50%
12
+ #
13
+ # Messages that exhaust all retries are stored in the dead letter queue
14
+ # and retrievable via the sync :dead_letters query.
15
+ #
16
+ # NOTE: The handler blocks the agent thread for the duration of all retries
17
+ # plus sleep intervals. For long-running retries, consider a dedicated
18
+ # RetryAgent instance per task.
19
+ #
20
+ # @example
21
+ # ref = RetryAgent.start
22
+ # ref.send(:with_retry,
23
+ # callable: ->(x:) { ExternalService.call(x) },
24
+ # args: { x: 42 },
25
+ # max_retries: 3,
26
+ # backoff: :exponential,
27
+ # base_delay: 0.5
28
+ # )
29
+ # dead = ref.call(:dead_letters) # => [] (on success)
30
+ class RetryAgent < Igniter::Agent
31
+ # Returned as a sync reply from :dead_letters.
32
+ DeadLetter = Struct.new(:callable, :args, :error, :attempts, :ts, keyword_init: true)
33
+
34
+ initial_state dead_letters: []
35
+
36
+ # Execute +callable+ with retry.
37
+ #
38
+ # Payload keys:
39
+ # callable [#call] — required; receives **args
40
+ # args [Hash] — keyword arguments for callable (default: {})
41
+ # max_retries [Integer] — maximum retry count (default: 3)
42
+ # backoff [Symbol] — :immediate / :linear / :exponential (default: :exponential)
43
+ # base_delay [Float] — base sleep time in seconds (default: 1.0)
44
+ # jitter [Boolean] — add random ±50% jitter to delay (default: false)
45
+ on :with_retry do |state:, payload:|
46
+ agent = new
47
+ letter = agent.send(:run_with_retry, payload)
48
+ letter ? state.merge(dead_letters: state[:dead_letters] + [letter]) : state
49
+ end
50
+
51
+ # Sync query — returns Array<DeadLetter>.
52
+ on :dead_letters do |state:, **|
53
+ state[:dead_letters]
54
+ end
55
+
56
+ # Clear the dead letter queue.
57
+ on :clear_dead_letters do |state:, **|
58
+ state.merge(dead_letters: [])
59
+ end
60
+
61
+ private
62
+
63
+ def run_with_retry(payload)
64
+ callable = payload.fetch(:callable)
65
+ args = payload.fetch(:args, {})
66
+ max_retries = payload.fetch(:max_retries, 3).to_i
67
+ backoff = payload.fetch(:backoff, :exponential).to_sym
68
+ base_delay = payload.fetch(:base_delay, 1.0).to_f
69
+ jitter = payload.fetch(:jitter, false)
70
+
71
+ attempt = 0
72
+ begin
73
+ attempt += 1
74
+ callable.call(**args)
75
+ nil # success
76
+ rescue StandardError => e
77
+ if attempt <= max_retries
78
+ sleep compute_delay(backoff, base_delay, attempt, jitter)
79
+ retry
80
+ else
81
+ DeadLetter.new(callable: callable, args: args,
82
+ error: e.message, attempts: attempt,
83
+ ts: Time.now.to_i)
84
+ end
85
+ end
86
+ end
87
+
88
+ def compute_delay(strategy, base, attempt, jitter)
89
+ raw = case strategy
90
+ when :immediate then 0.0
91
+ when :linear then base * attempt
92
+ when :exponential then base * (2**(attempt - 1))
93
+ else 0.0
94
+ end
95
+ jitter ? raw * (0.5 + rand * 0.5) : raw
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Agents
5
+ # Interval-based job scheduler with cron-like semantics.
6
+ #
7
+ # Jobs are registered with a name, an interval (seconds), and a callable.
8
+ # A built-in schedule fires every second to advance due jobs.
9
+ # The :_tick handler is also exposed for deterministic testing.
10
+ #
11
+ # @example
12
+ # ref = CronAgent.start
13
+ # ref.send(:add_job,
14
+ # name: :cleanup,
15
+ # every: 3600,
16
+ # callable: -> { DataStore.purge_old_records }
17
+ # )
18
+ # status = ref.call(:list_jobs) # => [{ name: :cleanup, every: 3600, runs: 0 }]
19
+ class CronAgent < Igniter::Agent
20
+ # Returned by :list_jobs sync query.
21
+ JobInfo = Struct.new(:name, :every, :runs, :next_in, keyword_init: true)
22
+
23
+ initial_state jobs: {}
24
+
25
+ # Auto-advance due jobs every second.
26
+ schedule(:tick, every: 1.0) do |state:|
27
+ agent = new
28
+ agent.send(:advance_jobs, state)
29
+ end
30
+
31
+ # Register or replace a job.
32
+ #
33
+ # Payload keys:
34
+ # name [Symbol, String] — unique job identifier
35
+ # every [Numeric] — interval in seconds
36
+ # callable [#call] — called with no arguments when due
37
+ on :add_job do |state:, payload:|
38
+ name = payload.fetch(:name).to_sym
39
+ every = payload.fetch(:every).to_f
40
+ callable = payload.fetch(:callable)
41
+
42
+ job = {
43
+ name: name,
44
+ every: every,
45
+ callable: callable,
46
+ next_at: Time.now.to_f + every,
47
+ runs: 0
48
+ }
49
+ state.merge(jobs: state[:jobs].merge(name => job))
50
+ end
51
+
52
+ # Remove a job by name.
53
+ #
54
+ # Payload keys:
55
+ # name [Symbol, String]
56
+ on :remove_job do |state:, payload:|
57
+ name = payload.fetch(:name).to_sym
58
+ state.merge(jobs: state[:jobs].reject { |k, _| k == name })
59
+ end
60
+
61
+ # Sync query — list registered jobs.
62
+ #
63
+ # @return [Array<JobInfo>]
64
+ on :list_jobs do |state:, **|
65
+ now = Time.now.to_f
66
+ state[:jobs].values.map do |j|
67
+ JobInfo.new(
68
+ name: j[:name],
69
+ every: j[:every],
70
+ runs: j[:runs],
71
+ next_in: [j[:next_at] - now, 0].max.round(2)
72
+ )
73
+ end
74
+ end
75
+
76
+ # Manually advance jobs — useful for testing without real time delays.
77
+ # Pass +at:+ to simulate a specific point in time.
78
+ #
79
+ # Payload keys:
80
+ # at [Float, nil] — Unix timestamp to use as "now" (default: Time.now.to_f)
81
+ on :_tick do |state:, payload:|
82
+ agent = new
83
+ agent.send(:advance_jobs, state, payload[:at])
84
+ end
85
+
86
+ private
87
+
88
+ # Run all jobs whose next_at has passed and reschedule them.
89
+ # Errors in job callables are swallowed to keep the scheduler alive.
90
+ #
91
+ # @param state [Hash]
92
+ # @param now [Float, nil]
93
+ # @return [Hash] updated state
94
+ def advance_jobs(state, now = nil)
95
+ now = now || Time.now.to_f
96
+ jobs = state[:jobs].transform_values do |job|
97
+ next job if job[:next_at] > now
98
+
99
+ begin
100
+ job[:callable].call
101
+ rescue StandardError
102
+ nil # scheduler must not crash
103
+ end
104
+ job.merge(next_at: now + job[:every], runs: job[:runs] + 1)
105
+ end
106
+ state.merge(jobs: jobs)
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Standard library of ready-made Igniter agents.
4
+ #
5
+ # Usage:
6
+ # require "igniter/agents"
7
+ #
8
+ # Provides production-grade agents per domain:
9
+ #
10
+ # Reliability — Igniter::Agents::RetryAgent
11
+ # Pipeline — Igniter::Agents::BatchProcessorAgent
12
+ # Scheduling — Igniter::Agents::CronAgent
13
+ # AI/LLM — Igniter::Agents::RouterAgent
14
+ # Igniter::Agents::CriticAgent
15
+ # Igniter::Agents::PlannerAgent
16
+ # Igniter::Agents::ChainAgent
17
+ # Igniter::Agents::SelfReflectionAgent
18
+ # Igniter::Agents::ObserverAgent
19
+ # Igniter::Agents::EvaluatorAgent
20
+ # Igniter::Agents::EvolutionAgent
21
+ # Proactive — Igniter::Agents::ProactiveAgent (base)
22
+ # Igniter::Agents::AlertAgent
23
+ # Igniter::Agents::HealthCheckAgent
24
+ # Observability — Igniter::Agents::MetricsAgent
25
+ #
26
+ require_relative "integrations/agents"
27
+ require_relative "agents/reliability/retry_agent"
28
+ require_relative "agents/pipeline/batch_processor_agent"
29
+ require_relative "agents/scheduling/cron_agent"
30
+ require_relative "agents/ai/router_agent"
31
+ require_relative "agents/ai/critic_agent"
32
+ require_relative "agents/ai/planner_agent"
33
+ require_relative "agents/ai/chain_agent"
34
+ require_relative "agents/ai/self_reflection_agent"
35
+ require_relative "agents/ai/observer_agent"
36
+ require_relative "agents/ai/evaluator_agent"
37
+ require_relative "agents/ai/evolution_agent"
38
+ require_relative "agents/proactive_agent"
39
+ require_relative "agents/ai/alert_agent"
40
+ require_relative "agents/ai/health_check_agent"
41
+ require_relative "agents/observability/metrics_agent"
42
+
43
+ module Igniter
44
+ module Agents
45
+ # Convenience method — list all registered stdlib agents.
46
+ #
47
+ # @return [Array<Class>]
48
+ def self.all
49
+ [RetryAgent, BatchProcessorAgent, CronAgent,
50
+ RouterAgent, CriticAgent, PlannerAgent, ChainAgent,
51
+ SelfReflectionAgent, ObserverAgent, EvaluatorAgent, EvolutionAgent,
52
+ AlertAgent, HealthCheckAgent,
53
+ MetricsAgent]
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ class Application
5
+ # Unified configuration object for an Igniter::Application.
6
+ # Wraps server-level settings in a single place.
7
+ # Call #to_server_config to get a Server::Config for HttpServer / RackApp.
8
+ class AppConfig
9
+ attr_accessor :host, :port, :store, :log_format, :drain_timeout, :metrics_collector
10
+
11
+ def initialize
12
+ @host = "0.0.0.0"
13
+ @port = 4567
14
+ @store = nil # nil → MemoryStore default inside Server::Config
15
+ @log_format = :text
16
+ @drain_timeout = 30
17
+ @metrics_collector = nil
18
+ end
19
+
20
+ def to_server_config
21
+ Igniter::Server::Config.new.tap do |sc|
22
+ sc.host = host
23
+ sc.port = port
24
+ sc.store = store if store
25
+ sc.log_format = log_format
26
+ sc.drain_timeout = drain_timeout
27
+ sc.metrics_collector = metrics_collector
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ class Application
5
+ # Eagerly loads all .rb files matching a glob path.
6
+ # Used by Application#executors_path / #contracts_path.
7
+ class Autoloader
8
+ def initialize(base_dir:)
9
+ @base_dir = File.expand_path(base_dir)
10
+ end
11
+
12
+ def load_path(path)
13
+ full = File.expand_path(path, @base_dir)
14
+ Dir.glob("#{full}/**/*.rb").sort.each { |f| require f }
15
+ end
16
+ end
17
+ end
18
+ end