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.
- checksums.yaml +4 -4
- data/README.md +217 -0
- data/docs/APPLICATION_V1.md +253 -0
- data/docs/CAPABILITIES_V1.md +207 -0
- data/docs/CONSENSUS_V1.md +477 -0
- data/docs/CONTENT_ADDRESSING_V1.md +221 -0
- data/docs/DATAFLOW_V1.md +274 -0
- data/docs/MESH_V1.md +732 -0
- data/docs/NODE_CACHE_V1.md +324 -0
- data/docs/PROACTIVE_AGENTS_V1.md +293 -0
- data/docs/SERVER_V1.md +200 -1
- data/docs/SKILLS_V1.md +213 -0
- data/docs/STORE_ADAPTERS.md +41 -13
- data/docs/TEMPORAL_V1.md +174 -0
- data/docs/TOOLS_V1.md +347 -0
- data/docs/TRANSCRIPTION_V1.md +403 -0
- data/examples/README.md +37 -0
- data/examples/consensus.rb +239 -0
- data/examples/dataflow.rb +308 -0
- data/examples/elocal_webhook.rb +1 -0
- data/examples/llm_tools.rb +237 -0
- data/examples/mesh.rb +239 -0
- data/examples/mesh_discovery.rb +267 -0
- data/examples/mesh_gossip.rb +162 -0
- data/examples/ringcentral_routing.rb +1 -1
- data/lib/igniter/agents/ai/alert_agent.rb +111 -0
- data/lib/igniter/agents/ai/chain_agent.rb +127 -0
- data/lib/igniter/agents/ai/critic_agent.rb +163 -0
- data/lib/igniter/agents/ai/evaluator_agent.rb +193 -0
- data/lib/igniter/agents/ai/evolution_agent.rb +286 -0
- data/lib/igniter/agents/ai/health_check_agent.rb +122 -0
- data/lib/igniter/agents/ai/observer_agent.rb +184 -0
- data/lib/igniter/agents/ai/planner_agent.rb +210 -0
- data/lib/igniter/agents/ai/router_agent.rb +131 -0
- data/lib/igniter/agents/ai/self_reflection_agent.rb +175 -0
- data/lib/igniter/agents/observability/metrics_agent.rb +130 -0
- data/lib/igniter/agents/pipeline/batch_processor_agent.rb +131 -0
- data/lib/igniter/agents/proactive_agent.rb +208 -0
- data/lib/igniter/agents/reliability/retry_agent.rb +99 -0
- data/lib/igniter/agents/scheduling/cron_agent.rb +110 -0
- data/lib/igniter/agents.rb +56 -0
- data/lib/igniter/application/app_config.rb +32 -0
- data/lib/igniter/application/autoloader.rb +18 -0
- data/lib/igniter/application/generator.rb +157 -0
- data/lib/igniter/application/scheduler.rb +109 -0
- data/lib/igniter/application/yml_loader.rb +39 -0
- data/lib/igniter/application.rb +174 -0
- data/lib/igniter/capabilities.rb +68 -0
- data/lib/igniter/compiler/validators/dependencies_validator.rb +50 -2
- data/lib/igniter/compiler/validators/remote_validator.rb +2 -0
- data/lib/igniter/consensus/cluster.rb +183 -0
- data/lib/igniter/consensus/errors.rb +14 -0
- data/lib/igniter/consensus/executors.rb +43 -0
- data/lib/igniter/consensus/node.rb +320 -0
- data/lib/igniter/consensus/read_query.rb +30 -0
- data/lib/igniter/consensus/state_machine.rb +58 -0
- data/lib/igniter/consensus.rb +58 -0
- data/lib/igniter/content_addressing.rb +133 -0
- data/lib/igniter/contract.rb +12 -0
- data/lib/igniter/dataflow/aggregate_operators.rb +147 -0
- data/lib/igniter/dataflow/aggregate_state.rb +77 -0
- data/lib/igniter/dataflow/diff.rb +37 -0
- data/lib/igniter/dataflow/diff_state.rb +81 -0
- data/lib/igniter/dataflow/incremental_collection_result.rb +39 -0
- data/lib/igniter/dataflow/window_filter.rb +48 -0
- data/lib/igniter/dataflow.rb +65 -0
- data/lib/igniter/dsl/contract_builder.rb +71 -7
- data/lib/igniter/executor.rb +60 -0
- data/lib/igniter/extensions/capabilities.rb +39 -0
- data/lib/igniter/extensions/content_addressing.rb +5 -0
- data/lib/igniter/extensions/dataflow.rb +117 -0
- data/lib/igniter/extensions/mesh.rb +31 -0
- data/lib/igniter/fingerprint.rb +43 -0
- data/lib/igniter/integrations/llm/config.rb +48 -4
- data/lib/igniter/integrations/llm/executor.rb +221 -28
- data/lib/igniter/integrations/llm/providers/anthropic.rb +37 -4
- data/lib/igniter/integrations/llm/providers/openai.rb +34 -5
- data/lib/igniter/integrations/llm/transcription/providers/assemblyai.rb +200 -0
- data/lib/igniter/integrations/llm/transcription/providers/base.rb +122 -0
- data/lib/igniter/integrations/llm/transcription/providers/deepgram.rb +162 -0
- data/lib/igniter/integrations/llm/transcription/providers/openai.rb +102 -0
- data/lib/igniter/integrations/llm/transcription/transcriber.rb +145 -0
- data/lib/igniter/integrations/llm/transcription/transcript_result.rb +29 -0
- data/lib/igniter/integrations/llm.rb +37 -1
- data/lib/igniter/memory/agent_memory.rb +104 -0
- data/lib/igniter/memory/episode.rb +29 -0
- data/lib/igniter/memory/fact.rb +27 -0
- data/lib/igniter/memory/memorable.rb +90 -0
- data/lib/igniter/memory/reflection_cycle.rb +96 -0
- data/lib/igniter/memory/reflection_record.rb +28 -0
- data/lib/igniter/memory/store.rb +115 -0
- data/lib/igniter/memory/stores/in_memory.rb +136 -0
- data/lib/igniter/memory/stores/sqlite.rb +284 -0
- data/lib/igniter/memory.rb +80 -0
- data/lib/igniter/mesh/announcer.rb +55 -0
- data/lib/igniter/mesh/config.rb +45 -0
- data/lib/igniter/mesh/discovery.rb +39 -0
- data/lib/igniter/mesh/errors.rb +31 -0
- data/lib/igniter/mesh/gossip.rb +47 -0
- data/lib/igniter/mesh/peer.rb +21 -0
- data/lib/igniter/mesh/peer_registry.rb +51 -0
- data/lib/igniter/mesh/poller.rb +77 -0
- data/lib/igniter/mesh/router.rb +109 -0
- data/lib/igniter/mesh.rb +85 -0
- data/lib/igniter/metrics/collector.rb +131 -0
- data/lib/igniter/metrics/prometheus_exporter.rb +104 -0
- data/lib/igniter/metrics/snapshot.rb +8 -0
- data/lib/igniter/metrics.rb +37 -0
- data/lib/igniter/model/aggregate_node.rb +34 -0
- data/lib/igniter/model/collection_node.rb +3 -2
- data/lib/igniter/model/compute_node.rb +13 -0
- data/lib/igniter/model/remote_node.rb +18 -2
- data/lib/igniter/node_cache.rb +231 -0
- data/lib/igniter/replication/bootstrapper.rb +61 -0
- data/lib/igniter/replication/bootstrappers/gem.rb +32 -0
- data/lib/igniter/replication/bootstrappers/git.rb +39 -0
- data/lib/igniter/replication/bootstrappers/tarball.rb +56 -0
- data/lib/igniter/replication/expansion_plan.rb +38 -0
- data/lib/igniter/replication/expansion_planner.rb +142 -0
- data/lib/igniter/replication/manifest.rb +45 -0
- data/lib/igniter/replication/network_topology.rb +123 -0
- data/lib/igniter/replication/node_role.rb +42 -0
- data/lib/igniter/replication/reflective_replication_agent.rb +238 -0
- data/lib/igniter/replication/replication_agent.rb +87 -0
- data/lib/igniter/replication/role_registry.rb +73 -0
- data/lib/igniter/replication/ssh_session.rb +77 -0
- data/lib/igniter/replication.rb +54 -0
- data/lib/igniter/runtime/execution.rb +18 -0
- data/lib/igniter/runtime/input_validator.rb +6 -2
- data/lib/igniter/runtime/resolver.rb +254 -16
- data/lib/igniter/runtime/stores/redis_store.rb +41 -4
- data/lib/igniter/server/client.rb +44 -1
- data/lib/igniter/server/config.rb +13 -6
- data/lib/igniter/server/handlers/event_handler.rb +4 -0
- data/lib/igniter/server/handlers/execute_handler.rb +6 -0
- data/lib/igniter/server/handlers/liveness_handler.rb +20 -0
- data/lib/igniter/server/handlers/manifest_handler.rb +34 -0
- data/lib/igniter/server/handlers/metrics_handler.rb +51 -0
- data/lib/igniter/server/handlers/peers_handler.rb +115 -0
- data/lib/igniter/server/handlers/readiness_handler.rb +47 -0
- data/lib/igniter/server/http_server.rb +54 -17
- data/lib/igniter/server/router.rb +54 -21
- data/lib/igniter/server/server_logger.rb +52 -0
- data/lib/igniter/server.rb +6 -0
- data/lib/igniter/skill/feedback.rb +116 -0
- data/lib/igniter/skill/output_schema.rb +110 -0
- data/lib/igniter/skill.rb +218 -0
- data/lib/igniter/temporal.rb +84 -0
- data/lib/igniter/tool/discoverable.rb +151 -0
- data/lib/igniter/tool.rb +52 -0
- data/lib/igniter/tool_registry.rb +144 -0
- data/lib/igniter/version.rb +1 -1
- data/lib/igniter.rb +17 -0
- 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
|