igniter 0.4.3 → 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/incremental.rb +142 -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/incremental.rb +50 -0
- data/lib/igniter/extensions/mesh.rb +31 -0
- data/lib/igniter/fingerprint.rb +43 -0
- data/lib/igniter/incremental/formatter.rb +81 -0
- data/lib/igniter/incremental/result.rb +69 -0
- data/lib/igniter/incremental/tracker.rb +108 -0
- data/lib/igniter/incremental.rb +50 -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/cache.rb +35 -6
- data/lib/igniter/runtime/execution.rb +26 -2
- data/lib/igniter/runtime/input_validator.rb +6 -2
- data/lib/igniter/runtime/node_state.rb +7 -2
- data/lib/igniter/runtime/resolver.rb +323 -31
- 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 +128 -1
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Memory
|
|
5
|
+
# Immutable value object representing the output of a reflection cycle.
|
|
6
|
+
#
|
|
7
|
+
# A ReflectionRecord captures the summary and optional system-prompt patch
|
|
8
|
+
# produced by a ReflectionCycle run. It can be applied to update an agent's
|
|
9
|
+
# system prompt or used as an audit trail.
|
|
10
|
+
#
|
|
11
|
+
# @!attribute [r] id
|
|
12
|
+
# @return [Integer] unique identifier within the store
|
|
13
|
+
# @!attribute [r] agent_id
|
|
14
|
+
# @return [String] identifier of the agent this reflection belongs to
|
|
15
|
+
# @!attribute [r] ts
|
|
16
|
+
# @return [Integer] Unix timestamp when the reflection was recorded
|
|
17
|
+
# @!attribute [r] summary
|
|
18
|
+
# @return [String] human-readable summary of findings
|
|
19
|
+
# @!attribute [r] system_patch
|
|
20
|
+
# @return [String, nil] optional suggested replacement/patch for system prompt
|
|
21
|
+
# @!attribute [r] applied
|
|
22
|
+
# @return [Boolean] whether this reflection has been applied to the agent
|
|
23
|
+
ReflectionRecord = Struct.new(
|
|
24
|
+
:id, :agent_id, :ts, :summary, :system_patch, :applied,
|
|
25
|
+
keyword_init: true
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Memory
|
|
5
|
+
# Abstract base class defining the Store interface for episodic memory.
|
|
6
|
+
#
|
|
7
|
+
# Concrete adapters must subclass Store and implement all public methods.
|
|
8
|
+
# All methods raise +NotImplementedError+ by default.
|
|
9
|
+
#
|
|
10
|
+
# == Interface
|
|
11
|
+
#
|
|
12
|
+
# store.record(agent_id:, type:, content:) # => Episode
|
|
13
|
+
# store.episodes(agent_id:, last: 50) # => Array<Episode>
|
|
14
|
+
# store.retrieve(agent_id:, query: "keyword") # => Array<Episode>
|
|
15
|
+
# store.store_fact(agent_id:, key:, value:)
|
|
16
|
+
# store.facts(agent_id:) # => Hash{key => Fact}
|
|
17
|
+
# store.record_reflection(agent_id:, summary:) # => ReflectionRecord
|
|
18
|
+
# store.reflections(agent_id:) # => Array<ReflectionRecord>
|
|
19
|
+
# store.apply_reflection(id:) # => true/false
|
|
20
|
+
# store.clear(agent_id:)
|
|
21
|
+
class Store
|
|
22
|
+
# Record an episode. Returns the persisted Episode.
|
|
23
|
+
#
|
|
24
|
+
# @param agent_id [String] identifier of the owning agent
|
|
25
|
+
# @param type [String, Symbol] category tag for the episode
|
|
26
|
+
# @param content [String] textual description of the event
|
|
27
|
+
# @param session_id [String, nil] optional session grouping key
|
|
28
|
+
# @param outcome [String, nil] result label, e.g. "success"/"failure"
|
|
29
|
+
# @param importance [Float] relevance weight 0.0-1.0 (default 0.5)
|
|
30
|
+
# @return [Episode]
|
|
31
|
+
def record(agent_id:, type:, content:, session_id: nil, outcome: nil, importance: 0.5) # rubocop:disable Metrics/ParameterLists
|
|
32
|
+
raise NotImplementedError, "#{self.class}#record not implemented"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Return episodes for an agent, newest last.
|
|
36
|
+
#
|
|
37
|
+
# @param agent_id [String] identifier of the agent
|
|
38
|
+
# @param last [Integer] maximum number of episodes to return
|
|
39
|
+
# @param type [String, Symbol, nil] optional type filter
|
|
40
|
+
# @return [Array<Episode>]
|
|
41
|
+
def episodes(agent_id:, last: 50, type: nil)
|
|
42
|
+
raise NotImplementedError, "#{self.class}#episodes not implemented"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Keyword-search episodes. Returns Array<Episode>.
|
|
46
|
+
#
|
|
47
|
+
# When +query+ is nil, returns the last +limit+ episodes.
|
|
48
|
+
# When +query+ is provided, filters by case-insensitive substring or FTS match.
|
|
49
|
+
#
|
|
50
|
+
# @param agent_id [String]
|
|
51
|
+
# @param query [String, nil] search term
|
|
52
|
+
# @param limit [Integer] maximum results (default 10)
|
|
53
|
+
# @param type [String, Symbol, nil] optional type filter
|
|
54
|
+
# @return [Array<Episode>]
|
|
55
|
+
def retrieve(agent_id:, query: nil, limit: 10, type: nil)
|
|
56
|
+
raise NotImplementedError, "#{self.class}#retrieve not implemented"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Upsert a fact for an agent.
|
|
60
|
+
#
|
|
61
|
+
# @param agent_id [String] identifier of the owning agent
|
|
62
|
+
# @param key [String] fact name
|
|
63
|
+
# @param value [Object] fact value
|
|
64
|
+
# @param confidence [Float] confidence score 0.0-1.0 (default 1.0)
|
|
65
|
+
# @return [Fact]
|
|
66
|
+
def store_fact(agent_id:, key:, value:, confidence: 1.0)
|
|
67
|
+
raise NotImplementedError, "#{self.class}#store_fact not implemented"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Returns all facts for an agent as a Hash keyed by string key.
|
|
71
|
+
#
|
|
72
|
+
# @param agent_id [String]
|
|
73
|
+
# @return [Hash{String => Fact}]
|
|
74
|
+
def facts(agent_id:)
|
|
75
|
+
raise NotImplementedError, "#{self.class}#facts not implemented"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Store a reflection record. Returns the persisted ReflectionRecord.
|
|
79
|
+
#
|
|
80
|
+
# @param agent_id [String] owning agent identifier
|
|
81
|
+
# @param summary [String] human-readable reflection summary
|
|
82
|
+
# @param system_patch [String, nil] optional suggested system prompt patch
|
|
83
|
+
# @param applied [Boolean] whether already applied (default false)
|
|
84
|
+
# @return [ReflectionRecord]
|
|
85
|
+
def record_reflection(agent_id:, summary:, system_patch: nil, applied: false)
|
|
86
|
+
raise NotImplementedError, "#{self.class}#record_reflection not implemented"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Returns reflection records for an agent.
|
|
90
|
+
#
|
|
91
|
+
# @param agent_id [String]
|
|
92
|
+
# @param applied [Boolean, nil] nil returns all; true/false filters by applied flag
|
|
93
|
+
# @return [Array<ReflectionRecord>]
|
|
94
|
+
def reflections(agent_id:, applied: nil)
|
|
95
|
+
raise NotImplementedError, "#{self.class}#reflections not implemented"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Mark a reflection as applied.
|
|
99
|
+
#
|
|
100
|
+
# @param id [Integer] reflection identifier
|
|
101
|
+
# @return [Boolean] true if found and updated, false if not found
|
|
102
|
+
def apply_reflection(id:)
|
|
103
|
+
raise NotImplementedError, "#{self.class}#apply_reflection not implemented"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Clear all stored data for an agent.
|
|
107
|
+
#
|
|
108
|
+
# @param agent_id [String]
|
|
109
|
+
# @return [void]
|
|
110
|
+
def clear(agent_id:)
|
|
111
|
+
raise NotImplementedError, "#{self.class}#clear not implemented"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Memory
|
|
5
|
+
module Stores
|
|
6
|
+
# Thread-safe in-memory implementation of the Store interface.
|
|
7
|
+
#
|
|
8
|
+
# Stores all data in process memory. Suitable for testing and
|
|
9
|
+
# single-process applications where persistence is not required.
|
|
10
|
+
# All operations are protected by a single Mutex for thread safety.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# store = Igniter::Memory::Stores::InMemory.new
|
|
14
|
+
# ep = store.record(agent_id: "bot:1", type: :tool_call, content: "searched web")
|
|
15
|
+
# store.episodes(agent_id: "bot:1") # => [ep]
|
|
16
|
+
class InMemory < Store
|
|
17
|
+
def initialize # rubocop:disable Lint/MissingSuper
|
|
18
|
+
@episodes = []
|
|
19
|
+
@facts = {}
|
|
20
|
+
@reflections = []
|
|
21
|
+
@seq = 0
|
|
22
|
+
@mutex = Mutex.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @see Store#record
|
|
26
|
+
def record(agent_id:, type:, content:, session_id: nil, outcome: nil, importance: 0.5) # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
|
|
27
|
+
@mutex.synchronize do
|
|
28
|
+
ep = Episode.new(
|
|
29
|
+
id: next_id,
|
|
30
|
+
agent_id: agent_id,
|
|
31
|
+
session_id: session_id,
|
|
32
|
+
ts: Time.now.to_i,
|
|
33
|
+
type: type,
|
|
34
|
+
content: content,
|
|
35
|
+
outcome: outcome,
|
|
36
|
+
importance: importance
|
|
37
|
+
)
|
|
38
|
+
@episodes << ep
|
|
39
|
+
ep
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @see Store#episodes
|
|
44
|
+
def episodes(agent_id:, last: 50, type: nil)
|
|
45
|
+
@mutex.synchronize do
|
|
46
|
+
result = @episodes.select { |e| e.agent_id == agent_id }
|
|
47
|
+
result = result.select { |e| e.type == type } if type
|
|
48
|
+
result.last(last)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @see Store#retrieve
|
|
53
|
+
def retrieve(agent_id:, query: nil, limit: 10, type: nil)
|
|
54
|
+
eps = episodes(agent_id: agent_id, last: 1000, type: type)
|
|
55
|
+
return eps.last(limit) unless query
|
|
56
|
+
|
|
57
|
+
q = query.to_s.downcase
|
|
58
|
+
eps.select { |e| e.content.to_s.downcase.include?(q) }.last(limit)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# @see Store#store_fact
|
|
62
|
+
def store_fact(agent_id:, key:, value:, confidence: 1.0) # rubocop:disable Metrics/MethodLength
|
|
63
|
+
@mutex.synchronize do
|
|
64
|
+
@facts[agent_id] ||= {}
|
|
65
|
+
fact = Fact.new(
|
|
66
|
+
id: next_id,
|
|
67
|
+
agent_id: agent_id,
|
|
68
|
+
key: key.to_s,
|
|
69
|
+
value: value,
|
|
70
|
+
confidence: confidence,
|
|
71
|
+
updated_at: Time.now.to_i
|
|
72
|
+
)
|
|
73
|
+
@facts[agent_id][key.to_s] = fact
|
|
74
|
+
fact
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @see Store#facts
|
|
79
|
+
def facts(agent_id:)
|
|
80
|
+
@mutex.synchronize { (@facts[agent_id] || {}).dup }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @see Store#record_reflection
|
|
84
|
+
def record_reflection(agent_id:, summary:, system_patch: nil, applied: false) # rubocop:disable Metrics/MethodLength
|
|
85
|
+
@mutex.synchronize do
|
|
86
|
+
rec = ReflectionRecord.new(
|
|
87
|
+
id: next_id,
|
|
88
|
+
agent_id: agent_id,
|
|
89
|
+
ts: Time.now.to_i,
|
|
90
|
+
summary: summary,
|
|
91
|
+
system_patch: system_patch,
|
|
92
|
+
applied: applied
|
|
93
|
+
)
|
|
94
|
+
@reflections << rec
|
|
95
|
+
rec
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @see Store#reflections
|
|
100
|
+
def reflections(agent_id:, applied: nil)
|
|
101
|
+
@mutex.synchronize do
|
|
102
|
+
result = @reflections.select { |r| r.agent_id == agent_id }
|
|
103
|
+
applied.nil? ? result : result.select { |r| r.applied == applied }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @see Store#apply_reflection
|
|
108
|
+
def apply_reflection(id:)
|
|
109
|
+
@mutex.synchronize do
|
|
110
|
+
rec = @reflections.find { |r| r.id == id }
|
|
111
|
+
return false unless rec
|
|
112
|
+
|
|
113
|
+
idx = @reflections.index(rec)
|
|
114
|
+
@reflections[idx] = ReflectionRecord.new(**rec.to_h.merge(applied: true))
|
|
115
|
+
true
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# @see Store#clear
|
|
120
|
+
def clear(agent_id:)
|
|
121
|
+
@mutex.synchronize do
|
|
122
|
+
@episodes.reject! { |e| e.agent_id == agent_id }
|
|
123
|
+
@facts.delete(agent_id)
|
|
124
|
+
@reflections.reject! { |r| r.agent_id == agent_id }
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def next_id
|
|
131
|
+
@seq += 1
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Memory
|
|
5
|
+
module Stores
|
|
6
|
+
# SQLite-backed persistent Store implementation.
|
|
7
|
+
#
|
|
8
|
+
# Uses SQLite3 with FTS5 for fast full-text search on episode content.
|
|
9
|
+
# Requires the +sqlite3+ gem (soft dependency — not declared in the gemspec).
|
|
10
|
+
# Raises +Igniter::Memory::ConfigurationError+ if the gem is not available.
|
|
11
|
+
#
|
|
12
|
+
# @example In-memory SQLite (for tests)
|
|
13
|
+
# store = Igniter::Memory::Stores::SQLite.new(path: ":memory:")
|
|
14
|
+
#
|
|
15
|
+
# @example Persistent file
|
|
16
|
+
# store = Igniter::Memory::Stores::SQLite.new(path: "/tmp/agent_memory.db")
|
|
17
|
+
class SQLite < Store # rubocop:disable Metrics/ClassLength
|
|
18
|
+
# @param path [String] file path for the database, or ":memory:" for transient storage
|
|
19
|
+
def initialize(path:) # rubocop:disable Lint/MissingSuper
|
|
20
|
+
require "sqlite3"
|
|
21
|
+
rescue LoadError
|
|
22
|
+
raise ConfigurationError,
|
|
23
|
+
"SQLite store requires the 'sqlite3' gem. Add it to your Gemfile: gem 'sqlite3'"
|
|
24
|
+
else
|
|
25
|
+
@mutex = Mutex.new
|
|
26
|
+
@db = ::SQLite3::Database.new(path)
|
|
27
|
+
@db.results_as_hash = true
|
|
28
|
+
create_schema!
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @see Store#record
|
|
32
|
+
def record(agent_id:, type:, content:, session_id: nil, outcome: nil, importance: 0.5) # rubocop:disable Metrics/ParameterLists
|
|
33
|
+
@mutex.synchronize do
|
|
34
|
+
@db.execute(
|
|
35
|
+
"INSERT INTO episodes (agent_id, session_id, ts, type, content, outcome, importance) " \
|
|
36
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
37
|
+
[agent_id, session_id, Time.now.to_i, type.to_s, content.to_s, outcome, importance.to_f]
|
|
38
|
+
)
|
|
39
|
+
row_to_episode(
|
|
40
|
+
@db.get_first_row("SELECT * FROM episodes WHERE id = last_insert_rowid()")
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @see Store#episodes
|
|
46
|
+
def episodes(agent_id:, last: 50, type: nil) # rubocop:disable Metrics/MethodLength
|
|
47
|
+
@mutex.synchronize do
|
|
48
|
+
rows = if type
|
|
49
|
+
@db.execute(
|
|
50
|
+
"SELECT * FROM episodes WHERE agent_id = ? AND type = ? ORDER BY ts ASC LIMIT ?",
|
|
51
|
+
[agent_id, type.to_s, last]
|
|
52
|
+
)
|
|
53
|
+
else
|
|
54
|
+
@db.execute(
|
|
55
|
+
"SELECT * FROM episodes WHERE agent_id = ? ORDER BY ts ASC LIMIT ?",
|
|
56
|
+
[agent_id, last]
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
rows.map { |r| row_to_episode(r) }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# @see Store#retrieve
|
|
64
|
+
def retrieve(agent_id:, query: nil, limit: 10, type: nil) # rubocop:disable Metrics/MethodLength
|
|
65
|
+
@mutex.synchronize do
|
|
66
|
+
if query
|
|
67
|
+
fts_retrieve(agent_id, query, limit, type)
|
|
68
|
+
else
|
|
69
|
+
rows = if type
|
|
70
|
+
@db.execute(
|
|
71
|
+
"SELECT * FROM episodes WHERE agent_id = ? AND type = ? ORDER BY ts ASC LIMIT ?",
|
|
72
|
+
[agent_id, type.to_s, limit]
|
|
73
|
+
)
|
|
74
|
+
else
|
|
75
|
+
@db.execute(
|
|
76
|
+
"SELECT * FROM episodes WHERE agent_id = ? ORDER BY ts ASC LIMIT ?",
|
|
77
|
+
[agent_id, limit]
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
rows.map { |r| row_to_episode(r) }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# @see Store#store_fact
|
|
86
|
+
def store_fact(agent_id:, key:, value:, confidence: 1.0) # rubocop:disable Metrics/MethodLength
|
|
87
|
+
@mutex.synchronize do
|
|
88
|
+
serialized = value.to_s
|
|
89
|
+
@db.execute(
|
|
90
|
+
"INSERT OR REPLACE INTO facts (agent_id, key, value, confidence, updated_at) " \
|
|
91
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
92
|
+
[agent_id, key.to_s, serialized, confidence.to_f, Time.now.to_i]
|
|
93
|
+
)
|
|
94
|
+
row_to_fact(
|
|
95
|
+
@db.get_first_row("SELECT * FROM facts WHERE agent_id = ? AND key = ?", [agent_id, key.to_s])
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# @see Store#facts
|
|
101
|
+
def facts(agent_id:)
|
|
102
|
+
@mutex.synchronize do
|
|
103
|
+
rows = @db.execute("SELECT * FROM facts WHERE agent_id = ?", [agent_id])
|
|
104
|
+
rows.each_with_object({}) do |row, hash|
|
|
105
|
+
fact = row_to_fact(row)
|
|
106
|
+
hash[fact.key] = fact
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# @see Store#record_reflection
|
|
112
|
+
def record_reflection(agent_id:, summary:, system_patch: nil, applied: false)
|
|
113
|
+
@mutex.synchronize do
|
|
114
|
+
@db.execute(
|
|
115
|
+
"INSERT INTO reflections (agent_id, ts, summary, system_patch, applied) " \
|
|
116
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
117
|
+
[agent_id, Time.now.to_i, summary, system_patch, applied ? 1 : 0]
|
|
118
|
+
)
|
|
119
|
+
row_to_reflection(
|
|
120
|
+
@db.get_first_row("SELECT * FROM reflections WHERE id = last_insert_rowid()")
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# @see Store#reflections
|
|
126
|
+
def reflections(agent_id:, applied: nil) # rubocop:disable Metrics/MethodLength
|
|
127
|
+
@mutex.synchronize do
|
|
128
|
+
rows = if applied.nil?
|
|
129
|
+
@db.execute("SELECT * FROM reflections WHERE agent_id = ? ORDER BY ts ASC", [agent_id])
|
|
130
|
+
else
|
|
131
|
+
@db.execute(
|
|
132
|
+
"SELECT * FROM reflections WHERE agent_id = ? AND applied = ? ORDER BY ts ASC",
|
|
133
|
+
[agent_id, applied ? 1 : 0]
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
rows.map { |r| row_to_reflection(r) }
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# @see Store#apply_reflection
|
|
141
|
+
def apply_reflection(id:)
|
|
142
|
+
@mutex.synchronize do
|
|
143
|
+
changes_before = @db.changes
|
|
144
|
+
@db.execute("UPDATE reflections SET applied = 1 WHERE id = ?", [id])
|
|
145
|
+
@db.changes > changes_before || @db.changes == 1
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @see Store#clear
|
|
150
|
+
def clear(agent_id:)
|
|
151
|
+
@mutex.synchronize do
|
|
152
|
+
@db.execute("DELETE FROM episodes WHERE agent_id = ?", [agent_id])
|
|
153
|
+
@db.execute("DELETE FROM facts WHERE agent_id = ?", [agent_id])
|
|
154
|
+
@db.execute("DELETE FROM reflections WHERE agent_id = ?", [agent_id])
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
def fts_retrieve(agent_id, query, limit, type) # rubocop:disable Metrics/MethodLength
|
|
161
|
+
rows = if type
|
|
162
|
+
@db.execute(
|
|
163
|
+
"SELECT e.* FROM episodes e " \
|
|
164
|
+
"JOIN episodes_fts fts ON fts.rowid = e.id " \
|
|
165
|
+
"WHERE fts.content MATCH ? AND e.agent_id = ? AND e.type = ? " \
|
|
166
|
+
"ORDER BY e.ts ASC LIMIT ?",
|
|
167
|
+
[query.to_s, agent_id, type.to_s, limit]
|
|
168
|
+
)
|
|
169
|
+
else
|
|
170
|
+
@db.execute(
|
|
171
|
+
"SELECT e.* FROM episodes e " \
|
|
172
|
+
"JOIN episodes_fts fts ON fts.rowid = e.id " \
|
|
173
|
+
"WHERE fts.content MATCH ? AND e.agent_id = ? " \
|
|
174
|
+
"ORDER BY e.ts ASC LIMIT ?",
|
|
175
|
+
[query.to_s, agent_id, limit]
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
rows.map { |r| row_to_episode(r) }
|
|
179
|
+
rescue ::SQLite3::Exception
|
|
180
|
+
# FTS5 match error (e.g. invalid query syntax) — fall back to LIKE
|
|
181
|
+
fallback_retrieve(agent_id, query, limit, type)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def fallback_retrieve(agent_id, query, limit, type) # rubocop:disable Metrics/MethodLength
|
|
185
|
+
q = "%#{query}%"
|
|
186
|
+
rows = if type
|
|
187
|
+
@db.execute(
|
|
188
|
+
"SELECT * FROM episodes WHERE agent_id = ? AND type = ? AND content LIKE ? " \
|
|
189
|
+
"ORDER BY ts ASC LIMIT ?",
|
|
190
|
+
[agent_id, type.to_s, q, limit]
|
|
191
|
+
)
|
|
192
|
+
else
|
|
193
|
+
@db.execute(
|
|
194
|
+
"SELECT * FROM episodes WHERE agent_id = ? AND content LIKE ? " \
|
|
195
|
+
"ORDER BY ts ASC LIMIT ?",
|
|
196
|
+
[agent_id, q, limit]
|
|
197
|
+
)
|
|
198
|
+
end
|
|
199
|
+
rows.map { |r| row_to_episode(r) }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def row_to_episode(row) # rubocop:disable Metrics/MethodLength
|
|
203
|
+
return nil unless row
|
|
204
|
+
|
|
205
|
+
Episode.new(
|
|
206
|
+
id: row["id"],
|
|
207
|
+
agent_id: row["agent_id"],
|
|
208
|
+
session_id: row["session_id"],
|
|
209
|
+
ts: row["ts"],
|
|
210
|
+
type: row["type"],
|
|
211
|
+
content: row["content"],
|
|
212
|
+
outcome: row["outcome"],
|
|
213
|
+
importance: row["importance"]
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def row_to_fact(row)
|
|
218
|
+
return nil unless row
|
|
219
|
+
|
|
220
|
+
Fact.new(
|
|
221
|
+
id: row["id"],
|
|
222
|
+
agent_id: row["agent_id"],
|
|
223
|
+
key: row["key"],
|
|
224
|
+
value: row["value"],
|
|
225
|
+
confidence: row["confidence"],
|
|
226
|
+
updated_at: row["updated_at"]
|
|
227
|
+
)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def row_to_reflection(row)
|
|
231
|
+
return nil unless row
|
|
232
|
+
|
|
233
|
+
ReflectionRecord.new(
|
|
234
|
+
id: row["id"],
|
|
235
|
+
agent_id: row["agent_id"],
|
|
236
|
+
ts: row["ts"],
|
|
237
|
+
summary: row["summary"],
|
|
238
|
+
system_patch: row["system_patch"],
|
|
239
|
+
applied: row["applied"] == 1
|
|
240
|
+
)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def create_schema! # rubocop:disable Metrics/MethodLength
|
|
244
|
+
@db.execute_batch(<<~SQL)
|
|
245
|
+
CREATE TABLE IF NOT EXISTS episodes (
|
|
246
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
247
|
+
agent_id TEXT NOT NULL,
|
|
248
|
+
session_id TEXT,
|
|
249
|
+
ts INTEGER NOT NULL,
|
|
250
|
+
type TEXT NOT NULL,
|
|
251
|
+
content TEXT NOT NULL,
|
|
252
|
+
outcome TEXT,
|
|
253
|
+
importance REAL NOT NULL DEFAULT 0.5
|
|
254
|
+
);
|
|
255
|
+
CREATE INDEX IF NOT EXISTS idx_ep_agent ON episodes(agent_id, ts);
|
|
256
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS episodes_fts USING fts5(
|
|
257
|
+
content, content='episodes', content_rowid='id'
|
|
258
|
+
);
|
|
259
|
+
CREATE TRIGGER IF NOT EXISTS ep_ai AFTER INSERT ON episodes BEGIN
|
|
260
|
+
INSERT INTO episodes_fts(rowid, content) VALUES (new.id, new.content);
|
|
261
|
+
END;
|
|
262
|
+
CREATE TABLE IF NOT EXISTS facts (
|
|
263
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
264
|
+
agent_id TEXT NOT NULL,
|
|
265
|
+
key TEXT NOT NULL,
|
|
266
|
+
value TEXT,
|
|
267
|
+
confidence REAL DEFAULT 1.0,
|
|
268
|
+
updated_at INTEGER,
|
|
269
|
+
UNIQUE(agent_id, key)
|
|
270
|
+
);
|
|
271
|
+
CREATE TABLE IF NOT EXISTS reflections (
|
|
272
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
273
|
+
agent_id TEXT NOT NULL,
|
|
274
|
+
ts INTEGER NOT NULL,
|
|
275
|
+
summary TEXT,
|
|
276
|
+
system_patch TEXT,
|
|
277
|
+
applied INTEGER DEFAULT 0
|
|
278
|
+
);
|
|
279
|
+
SQL
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "igniter/errors"
|
|
4
|
+
require "igniter/memory/episode"
|
|
5
|
+
require "igniter/memory/fact"
|
|
6
|
+
require "igniter/memory/reflection_record"
|
|
7
|
+
require "igniter/memory/store"
|
|
8
|
+
require "igniter/memory/stores/in_memory"
|
|
9
|
+
require "igniter/memory/stores/sqlite"
|
|
10
|
+
require "igniter/memory/reflection_cycle"
|
|
11
|
+
require "igniter/memory/agent_memory"
|
|
12
|
+
require "igniter/memory/memorable"
|
|
13
|
+
|
|
14
|
+
module Igniter
|
|
15
|
+
# Pluggable episodic memory system for Agents and Skills.
|
|
16
|
+
#
|
|
17
|
+
# Agents and Skills can record what happened (episodes), store learned facts,
|
|
18
|
+
# and trigger reflection cycles that analyse past behaviour.
|
|
19
|
+
#
|
|
20
|
+
# == Quick start
|
|
21
|
+
#
|
|
22
|
+
# require "igniter/memory"
|
|
23
|
+
#
|
|
24
|
+
# class MyAgent < Igniter::Agent
|
|
25
|
+
# include Igniter::Memory::Memorable
|
|
26
|
+
# enable_memory # uses the global default InMemory store
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# == Custom store
|
|
30
|
+
#
|
|
31
|
+
# Igniter::Memory.default_store = Igniter::Memory::Stores::SQLite.new(path: "/tmp/mem.db")
|
|
32
|
+
#
|
|
33
|
+
# class MyAgent < Igniter::Agent
|
|
34
|
+
# include Igniter::Memory::Memorable
|
|
35
|
+
# enable_memory store: Igniter::Memory.default_store
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# == Global configuration
|
|
39
|
+
#
|
|
40
|
+
# Igniter::Memory.configure do |m|
|
|
41
|
+
# m.default_store = Igniter::Memory::Stores::SQLite.new(path: "/var/app/memory.db")
|
|
42
|
+
# end
|
|
43
|
+
module Memory
|
|
44
|
+
# Raised when a required dependency (e.g. sqlite3 gem) is missing or when
|
|
45
|
+
# Memory is misconfigured.
|
|
46
|
+
ConfigurationError = Class.new(Igniter::Error)
|
|
47
|
+
|
|
48
|
+
class << self
|
|
49
|
+
# Returns the global default store, creating an InMemory store on first access.
|
|
50
|
+
#
|
|
51
|
+
# @return [Store]
|
|
52
|
+
def default_store
|
|
53
|
+
@default_store ||= Stores::InMemory.new
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Override the global default store.
|
|
57
|
+
#
|
|
58
|
+
# @param store [Store]
|
|
59
|
+
# @return [Store]
|
|
60
|
+
attr_writer :default_store
|
|
61
|
+
|
|
62
|
+
# Yield self for block-style configuration.
|
|
63
|
+
#
|
|
64
|
+
# @example
|
|
65
|
+
# Igniter::Memory.configure do |m|
|
|
66
|
+
# m.default_store = Igniter::Memory::Stores::SQLite.new(path: "/tmp/mem.db")
|
|
67
|
+
# end
|
|
68
|
+
def configure
|
|
69
|
+
yield self
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Reset module-level state (primarily useful in tests).
|
|
73
|
+
#
|
|
74
|
+
# @return [void]
|
|
75
|
+
def reset!
|
|
76
|
+
@default_store = nil
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Mesh
|
|
5
|
+
# Announces this node's identity to seed nodes at startup and withdraws
|
|
6
|
+
# the registration on graceful shutdown.
|
|
7
|
+
#
|
|
8
|
+
# All network errors are swallowed — a seed being temporarily down must
|
|
9
|
+
# not prevent the local node from starting. The background Poller will
|
|
10
|
+
# re-register once the seed recovers.
|
|
11
|
+
class Announcer
|
|
12
|
+
def initialize(config)
|
|
13
|
+
@config = config
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# POST self-manifest to every configured seed. No-op if peer_name or
|
|
17
|
+
# local_url are not configured.
|
|
18
|
+
def announce_all
|
|
19
|
+
return unless announceable?
|
|
20
|
+
|
|
21
|
+
@config.seeds.each { |url| announce_to(url) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# DELETE self from every configured seed. Best-effort — errors are ignored.
|
|
25
|
+
def deannounce_all
|
|
26
|
+
return unless @config.peer_name
|
|
27
|
+
|
|
28
|
+
@config.seeds.each { |url| deannounce_from(url) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def announceable?
|
|
34
|
+
@config.peer_name && !@config.peer_name.to_s.strip.empty? &&
|
|
35
|
+
@config.local_url && !@config.local_url.to_s.strip.empty?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def announce_to(seed_url)
|
|
39
|
+
Igniter::Server::Client.new(seed_url, timeout: 5).register_peer(
|
|
40
|
+
name: @config.peer_name,
|
|
41
|
+
url: @config.local_url,
|
|
42
|
+
capabilities: @config.local_capabilities
|
|
43
|
+
)
|
|
44
|
+
rescue Igniter::Server::Client::ConnectionError
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def deannounce_from(seed_url)
|
|
49
|
+
Igniter::Server::Client.new(seed_url, timeout: 5).unregister_peer(@config.peer_name)
|
|
50
|
+
rescue Igniter::Server::Client::ConnectionError
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|