activematrix 0.0.5 → 0.0.8
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 +96 -28
- data/app/jobs/active_matrix/application_job.rb +11 -0
- data/app/models/active_matrix/agent/jobs/memory_reaper.rb +87 -0
- data/app/models/active_matrix/agent.rb +166 -0
- data/app/models/active_matrix/agent_store.rb +80 -0
- data/app/models/active_matrix/application_record.rb +15 -0
- data/app/models/active_matrix/chat_session.rb +105 -0
- data/app/models/active_matrix/knowledge_base.rb +100 -0
- data/exe/activematrix +7 -0
- data/lib/active_matrix/agent_manager.rb +160 -121
- data/lib/active_matrix/agent_registry.rb +25 -21
- data/lib/active_matrix/api.rb +8 -2
- data/lib/active_matrix/async_query.rb +58 -0
- data/lib/active_matrix/bot/base.rb +3 -3
- data/lib/active_matrix/bot/builtin_commands.rb +188 -0
- data/lib/active_matrix/bot/command_parser.rb +175 -0
- data/lib/active_matrix/cli.rb +273 -0
- data/lib/active_matrix/client.rb +21 -6
- data/lib/active_matrix/client_pool.rb +38 -27
- data/lib/active_matrix/daemon/probe_server.rb +118 -0
- data/lib/active_matrix/daemon/signal_handler.rb +156 -0
- data/lib/active_matrix/daemon/worker.rb +109 -0
- data/lib/active_matrix/daemon.rb +236 -0
- data/lib/active_matrix/engine.rb +18 -0
- data/lib/active_matrix/errors.rb +1 -1
- data/lib/active_matrix/event_router.rb +61 -49
- data/lib/active_matrix/events.rb +1 -0
- data/lib/active_matrix/instrumentation.rb +148 -0
- data/lib/active_matrix/memory/agent_memory.rb +7 -21
- data/lib/active_matrix/memory/conversation_memory.rb +4 -20
- data/lib/active_matrix/memory/global_memory.rb +15 -30
- data/lib/active_matrix/message_dispatcher.rb +197 -0
- data/lib/active_matrix/metrics.rb +424 -0
- data/lib/active_matrix/presence_manager.rb +181 -0
- data/lib/active_matrix/railtie.rb +8 -0
- data/lib/active_matrix/telemetry.rb +134 -0
- data/lib/active_matrix/version.rb +1 -1
- data/lib/active_matrix.rb +18 -11
- data/lib/generators/active_matrix/install/install_generator.rb +3 -22
- data/lib/generators/active_matrix/install/templates/README +5 -2
- metadata +191 -31
- data/lib/generators/active_matrix/install/templates/agent_memory.rb +0 -47
- data/lib/generators/active_matrix/install/templates/conversation_context.rb +0 -72
- data/lib/generators/active_matrix/install/templates/create_agent_memories.rb +0 -17
- data/lib/generators/active_matrix/install/templates/create_conversation_contexts.rb +0 -21
- data/lib/generators/active_matrix/install/templates/create_global_memories.rb +0 -20
- data/lib/generators/active_matrix/install/templates/create_matrix_agents.rb +0 -26
- data/lib/generators/active_matrix/install/templates/global_memory.rb +0 -70
- data/lib/generators/active_matrix/install/templates/matrix_agent.rb +0 -127
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# <rails-lens:schema:begin>
|
|
4
|
+
# table = "active_matrix_chat_sessions"
|
|
5
|
+
# database_dialect = "PostgreSQL"
|
|
6
|
+
#
|
|
7
|
+
# columns = [
|
|
8
|
+
# { name = "id", type = "integer", pk = true, null = false },
|
|
9
|
+
# { name = "agent_id", type = "integer", null = false },
|
|
10
|
+
# { name = "user_id", type = "string", null = false },
|
|
11
|
+
# { name = "room_id", type = "string", null = false },
|
|
12
|
+
# { name = "context", type = "json" },
|
|
13
|
+
# { name = "message_history", type = "json" },
|
|
14
|
+
# { name = "last_message_at", type = "datetime" },
|
|
15
|
+
# { name = "message_count", type = "integer", null = false, default = "0" },
|
|
16
|
+
# { name = "created_at", type = "datetime", null = false },
|
|
17
|
+
# { name = "updated_at", type = "datetime", null = false }
|
|
18
|
+
# ]
|
|
19
|
+
#
|
|
20
|
+
# indexes = [
|
|
21
|
+
# { name = "index_active_matrix_chat_sessions_on_agent_id", columns = ["agent_id"] },
|
|
22
|
+
# { name = "index_active_matrix_chat_sessions_on_last_message_at", columns = ["last_message_at"] },
|
|
23
|
+
# { name = "index_chat_sessions_on_agent_user_room", columns = ["agent_id", "user_id", "room_id"], unique = true }
|
|
24
|
+
# ]
|
|
25
|
+
#
|
|
26
|
+
# foreign_keys = [
|
|
27
|
+
# { column = "agent_id", references_table = "active_matrix_agents", references_column = "id", name = "fk_rails_53457da357" }
|
|
28
|
+
# ]
|
|
29
|
+
#
|
|
30
|
+
# notes = ["agent:INVERSE_OF", "context:NOT_NULL", "message_history:NOT_NULL", "user_id:LIMIT", "room_id:LIMIT"]
|
|
31
|
+
# <rails-lens:schema:end>
|
|
32
|
+
module ActiveMatrix
|
|
33
|
+
class ChatSession < ApplicationRecord
|
|
34
|
+
self.table_name = 'active_matrix_chat_sessions'
|
|
35
|
+
|
|
36
|
+
belongs_to :agent, class_name: 'ActiveMatrix::Agent'
|
|
37
|
+
|
|
38
|
+
validates :user_id, presence: true
|
|
39
|
+
validates :room_id, presence: true
|
|
40
|
+
validates :user_id, uniqueness: { scope: %i[agent_id room_id] }
|
|
41
|
+
|
|
42
|
+
# Configuration
|
|
43
|
+
MAX_HISTORY_SIZE = 20
|
|
44
|
+
|
|
45
|
+
# Scopes
|
|
46
|
+
scope :recent, -> { order(last_message_at: :desc) }
|
|
47
|
+
scope :active, -> { where('last_message_at > ?', 1.hour.ago) }
|
|
48
|
+
scope :stale, -> { where(last_message_at: ...1.day.ago) }
|
|
49
|
+
|
|
50
|
+
# Add a message to the history
|
|
51
|
+
def add_message(message_data)
|
|
52
|
+
messages = message_history['messages'] || []
|
|
53
|
+
|
|
54
|
+
# Add new message
|
|
55
|
+
messages << {
|
|
56
|
+
'event_id' => message_data[:event_id],
|
|
57
|
+
'sender' => message_data[:sender],
|
|
58
|
+
'content' => message_data[:content],
|
|
59
|
+
'timestamp' => message_data[:timestamp] || Time.current.to_i
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Keep only recent messages
|
|
63
|
+
messages = messages.last(MAX_HISTORY_SIZE)
|
|
64
|
+
|
|
65
|
+
# Update record
|
|
66
|
+
self.message_history = { 'messages' => messages }
|
|
67
|
+
self.last_message_at = Time.current
|
|
68
|
+
self.message_count = messages.size
|
|
69
|
+
save!
|
|
70
|
+
|
|
71
|
+
# Update cache
|
|
72
|
+
write_to_cache
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Get recent messages
|
|
76
|
+
def recent_messages(limit = 10)
|
|
77
|
+
messages = message_history['messages'] || []
|
|
78
|
+
messages.last(limit)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Clear old messages but keep context
|
|
82
|
+
def prune_history!
|
|
83
|
+
messages = message_history['messages'] || []
|
|
84
|
+
self.message_history = { 'messages' => messages.last(5) }
|
|
85
|
+
save!
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Cache integration
|
|
89
|
+
def cache_key
|
|
90
|
+
"conversation/#{agent_id}/#{user_id}/#{room_id}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def write_to_cache
|
|
94
|
+
Rails.cache.write(cache_key, {
|
|
95
|
+
context: context,
|
|
96
|
+
recent_messages: recent_messages,
|
|
97
|
+
last_message_at: last_message_at
|
|
98
|
+
}, expires_in: 1.hour)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.cleanup_stale!
|
|
102
|
+
stale.destroy_all
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# <rails-lens:schema:begin>
|
|
4
|
+
# table = "active_matrix_knowledge_bases"
|
|
5
|
+
# database_dialect = "PostgreSQL"
|
|
6
|
+
#
|
|
7
|
+
# columns = [
|
|
8
|
+
# { name = "id", type = "integer", pk = true, null = false },
|
|
9
|
+
# { name = "key", type = "string", null = false },
|
|
10
|
+
# { name = "value", type = "json" },
|
|
11
|
+
# { name = "category", type = "string" },
|
|
12
|
+
# { name = "expires_at", type = "datetime" },
|
|
13
|
+
# { name = "public_read", type = "boolean", null = false, default = "true" },
|
|
14
|
+
# { name = "public_write", type = "boolean", null = false, default = "false" },
|
|
15
|
+
# { name = "created_at", type = "datetime", null = false },
|
|
16
|
+
# { name = "updated_at", type = "datetime", null = false }
|
|
17
|
+
# ]
|
|
18
|
+
#
|
|
19
|
+
# indexes = [
|
|
20
|
+
# { name = "index_active_matrix_knowledge_bases_on_category", columns = ["category"] },
|
|
21
|
+
# { name = "index_active_matrix_knowledge_bases_on_expires_at", columns = ["expires_at"] },
|
|
22
|
+
# { name = "index_active_matrix_knowledge_bases_on_key", columns = ["key"], unique = true },
|
|
23
|
+
# { name = "index_active_matrix_knowledge_bases_on_public_read", columns = ["public_read"] },
|
|
24
|
+
# { name = "index_active_matrix_knowledge_bases_on_public_write", columns = ["public_write"] }
|
|
25
|
+
# ]
|
|
26
|
+
#
|
|
27
|
+
# notes = ["value:NOT_NULL", "category:NOT_NULL", "key:LIMIT", "category:LIMIT"]
|
|
28
|
+
# <rails-lens:schema:end>
|
|
29
|
+
module ActiveMatrix
|
|
30
|
+
class KnowledgeBase < ApplicationRecord
|
|
31
|
+
self.table_name = 'active_matrix_knowledge_bases'
|
|
32
|
+
|
|
33
|
+
validates :key, presence: true, uniqueness: true
|
|
34
|
+
|
|
35
|
+
scope :active, -> { where('expires_at IS NULL OR expires_at > ?', Time.current) }
|
|
36
|
+
scope :expired, -> { where(expires_at: ..Time.current) }
|
|
37
|
+
scope :by_category, ->(category) { where(category: category) }
|
|
38
|
+
scope :readable, -> { where(public_read: true) }
|
|
39
|
+
scope :writable, -> { where(public_write: true) }
|
|
40
|
+
|
|
41
|
+
def expired?
|
|
42
|
+
expires_at.present? && expires_at <= Time.current
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def readable_by?(agent)
|
|
46
|
+
public_read || (agent.is_a?(ActiveMatrix::Agent) && agent.admin?)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def writable_by?(agent)
|
|
50
|
+
public_write || (agent.is_a?(ActiveMatrix::Agent) && agent.admin?)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Cache integration
|
|
54
|
+
def cache_key
|
|
55
|
+
"global/#{key}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def write_to_cache
|
|
59
|
+
return unless active?
|
|
60
|
+
|
|
61
|
+
ttl = expires_at.present? ? expires_at - Time.current : nil
|
|
62
|
+
Rails.cache.write(cache_key, value, expires_in: ttl)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.get(key)
|
|
66
|
+
# Try cache first
|
|
67
|
+
cached = Rails.cache.read("global/#{key}")
|
|
68
|
+
return cached if cached.present?
|
|
69
|
+
|
|
70
|
+
# Fallback to database
|
|
71
|
+
memory = find_by(key: key)
|
|
72
|
+
return unless memory&.active?
|
|
73
|
+
|
|
74
|
+
memory.write_to_cache
|
|
75
|
+
memory.value
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.set(key, value, category: nil, expires_in: nil, public_read: true, public_write: false)
|
|
79
|
+
memory = find_or_initialize_by(key: key)
|
|
80
|
+
memory.value = value
|
|
81
|
+
memory.category = category
|
|
82
|
+
memory.expires_at = expires_in.present? ? Time.current + expires_in : nil
|
|
83
|
+
memory.public_read = public_read
|
|
84
|
+
memory.public_write = public_write
|
|
85
|
+
memory.save!
|
|
86
|
+
memory.write_to_cache
|
|
87
|
+
memory
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def self.cleanup_expired!
|
|
91
|
+
expired.destroy_all
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def active?
|
|
97
|
+
!expired?
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
data/exe/activematrix
ADDED
|
@@ -1,43 +1,88 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'singleton'
|
|
4
|
+
require 'async'
|
|
5
|
+
require 'async/barrier'
|
|
6
|
+
require 'async/semaphore'
|
|
4
7
|
|
|
5
8
|
module ActiveMatrix
|
|
6
|
-
# Manages the lifecycle of Matrix bot agents
|
|
9
|
+
# Manages the lifecycle of Matrix bot agents using async fibers
|
|
7
10
|
class AgentManager
|
|
8
11
|
include Singleton
|
|
9
12
|
include ActiveMatrix::Logging
|
|
10
13
|
|
|
14
|
+
# OpenTelemetry semantic conventions for messaging
|
|
15
|
+
OTEL_ATTRS = {
|
|
16
|
+
service: 'activematrix.agent_manager',
|
|
17
|
+
messaging_system: 'matrix'
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
11
20
|
attr_reader :registry, :config
|
|
12
21
|
|
|
13
22
|
def initialize
|
|
14
23
|
@registry = AgentRegistry.instance
|
|
15
24
|
@config = ActiveMatrix.config
|
|
16
|
-
@
|
|
17
|
-
@
|
|
25
|
+
@barrier = nil
|
|
26
|
+
@monitor_task = nil
|
|
27
|
+
@running = false
|
|
28
|
+
end
|
|
18
29
|
|
|
19
|
-
|
|
30
|
+
# Install signal handlers for graceful shutdown.
|
|
31
|
+
# Call this explicitly if you want the gem to handle SIGINT/SIGTERM.
|
|
32
|
+
# By default, signal handling is left to the host application.
|
|
33
|
+
def install_signal_handlers!
|
|
34
|
+
%w[INT TERM].each do |signal|
|
|
35
|
+
Signal.trap(signal) do
|
|
36
|
+
stop_all
|
|
37
|
+
exit # rubocop:disable Rails/Exit
|
|
38
|
+
end
|
|
39
|
+
end
|
|
20
40
|
end
|
|
21
41
|
|
|
22
42
|
# Start all agents marked as active in the database
|
|
43
|
+
# This is the main entry point - runs the async reactor
|
|
23
44
|
def start_all
|
|
24
|
-
|
|
45
|
+
agents = ActiveMatrix::Agent.where.not(state: :offline)
|
|
46
|
+
start_agents(agents)
|
|
47
|
+
end
|
|
25
48
|
|
|
26
|
-
|
|
49
|
+
# Start specific agents (used by daemon workers)
|
|
50
|
+
# @param agents [ActiveRecord::Relation, Array<Agent>] Agents to start
|
|
51
|
+
def start_agents(agents)
|
|
52
|
+
return if @running
|
|
27
53
|
|
|
28
|
-
|
|
29
|
-
agents.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
54
|
+
@running = true
|
|
55
|
+
agents_array = agents.respond_to?(:to_a) ? agents.to_a : agents
|
|
56
|
+
logger.info "Starting #{agents_array.size} agents..."
|
|
57
|
+
|
|
58
|
+
Sync do
|
|
59
|
+
@barrier = Async::Barrier.new
|
|
60
|
+
|
|
61
|
+
# Start the event router for routing Matrix events
|
|
62
|
+
EventRouter.instance.start
|
|
33
63
|
|
|
34
|
-
|
|
35
|
-
|
|
64
|
+
startup_delay = config.agent_startup_delay || 2
|
|
65
|
+
|
|
66
|
+
agents_array.each_with_index do |agent, index|
|
|
67
|
+
sleep(startup_delay) if index.positive?
|
|
68
|
+
start_agent(agent)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
start_monitor_task
|
|
72
|
+
|
|
73
|
+
logger.info "Started #{@registry.count} agents"
|
|
74
|
+
|
|
75
|
+
# Wait for all agent tasks to complete (blocks until shutdown)
|
|
76
|
+
@barrier.wait
|
|
77
|
+
ensure
|
|
78
|
+
@barrier&.stop
|
|
79
|
+
@running = false
|
|
80
|
+
end
|
|
36
81
|
end
|
|
37
82
|
|
|
38
|
-
# Start a specific agent
|
|
83
|
+
# Start a specific agent as an async task
|
|
39
84
|
def start_agent(agent)
|
|
40
|
-
return
|
|
85
|
+
return false unless @running
|
|
41
86
|
|
|
42
87
|
if @registry.running?(agent)
|
|
43
88
|
logger.warn "Agent #{agent.name} is already running"
|
|
@@ -47,51 +92,15 @@ module ActiveMatrix
|
|
|
47
92
|
logger.info "Starting agent: #{agent.name}"
|
|
48
93
|
|
|
49
94
|
begin
|
|
50
|
-
# Update state
|
|
51
95
|
agent.connect!
|
|
52
96
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
begin
|
|
58
|
-
# Create client and bot instance
|
|
59
|
-
client = create_client_for_agent(agent)
|
|
60
|
-
bot_class = agent.bot_class.constantize
|
|
61
|
-
bot_instance = bot_class.new(client)
|
|
62
|
-
|
|
63
|
-
# Register the agent
|
|
64
|
-
@registry.register(agent, bot_instance)
|
|
65
|
-
|
|
66
|
-
# Authenticate if needed
|
|
67
|
-
if agent.access_token.present?
|
|
68
|
-
client.access_token = agent.access_token
|
|
69
|
-
else
|
|
70
|
-
client.login(agent.username, agent.password)
|
|
71
|
-
agent.update(access_token: client.access_token)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Restore sync token if available
|
|
75
|
-
client.sync_token = agent.last_sync_token if agent.last_sync_token.present?
|
|
76
|
-
|
|
77
|
-
# Mark as online
|
|
78
|
-
agent.connection_established!
|
|
79
|
-
|
|
80
|
-
# Start the sync loop
|
|
81
|
-
client.start_listener_thread
|
|
82
|
-
client.instance_variable_get(:@sync_thread).join
|
|
83
|
-
rescue StandardError => e
|
|
84
|
-
logger.error "Error in agent #{agent.name}: #{e.message}"
|
|
85
|
-
logger.error e.backtrace.join("\n")
|
|
86
|
-
agent.encounter_error!
|
|
87
|
-
raise
|
|
88
|
-
ensure
|
|
89
|
-
@registry.unregister(agent)
|
|
90
|
-
agent.disconnect! if agent.may_disconnect?
|
|
91
|
-
end
|
|
97
|
+
task = @barrier.async do |subtask|
|
|
98
|
+
subtask.annotate "agent-#{agent.name}"
|
|
99
|
+
run_agent(agent)
|
|
92
100
|
end
|
|
93
101
|
|
|
94
|
-
|
|
102
|
+
# Store task reference for later control
|
|
103
|
+
@registry.register_task(agent, task)
|
|
95
104
|
true
|
|
96
105
|
rescue StandardError => e
|
|
97
106
|
logger.error "Failed to start agent #{agent.name}: #{e.message}"
|
|
@@ -109,18 +118,15 @@ module ActiveMatrix
|
|
|
109
118
|
|
|
110
119
|
begin
|
|
111
120
|
# Stop the client sync
|
|
112
|
-
client = entry[:instance]
|
|
113
|
-
client
|
|
121
|
+
client = entry[:instance]&.client
|
|
122
|
+
client&.stop_listener if client&.listening?
|
|
114
123
|
|
|
115
124
|
# Save sync token
|
|
116
|
-
agent.update(last_sync_token: client.sync_token) if client
|
|
125
|
+
agent.update(last_sync_token: client.sync_token) if client&.sync_token.present?
|
|
117
126
|
|
|
118
|
-
#
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
thread.kill
|
|
122
|
-
thread.join(5) # Wait up to 5 seconds
|
|
123
|
-
end
|
|
127
|
+
# Stop the async task gracefully
|
|
128
|
+
task = entry[:task]
|
|
129
|
+
task&.stop
|
|
124
130
|
|
|
125
131
|
# Update state
|
|
126
132
|
agent.disconnect! if agent.may_disconnect?
|
|
@@ -135,16 +141,17 @@ module ActiveMatrix
|
|
|
135
141
|
# Stop all running agents
|
|
136
142
|
def stop_all
|
|
137
143
|
logger.info 'Stopping all agents...'
|
|
138
|
-
@shutdown = true
|
|
139
144
|
|
|
140
|
-
# Stop monitor
|
|
141
|
-
@
|
|
145
|
+
# Stop monitor task
|
|
146
|
+
@monitor_task&.stop
|
|
142
147
|
|
|
143
|
-
# Stop
|
|
144
|
-
|
|
145
|
-
stop_agent(agent)
|
|
146
|
-
end
|
|
148
|
+
# Stop event router
|
|
149
|
+
EventRouter.instance.stop
|
|
147
150
|
|
|
151
|
+
# Stop all agent tasks via barrier
|
|
152
|
+
@barrier&.stop
|
|
153
|
+
|
|
154
|
+
@running = false
|
|
148
155
|
logger.info 'All agents stopped'
|
|
149
156
|
end
|
|
150
157
|
|
|
@@ -164,8 +171,8 @@ module ActiveMatrix
|
|
|
164
171
|
|
|
165
172
|
logger.info "Pausing agent: #{agent.name}"
|
|
166
173
|
|
|
167
|
-
client = entry[:instance]
|
|
168
|
-
client
|
|
174
|
+
client = entry[:instance]&.client
|
|
175
|
+
client&.stop_listener if client&.listening?
|
|
169
176
|
agent.pause!
|
|
170
177
|
|
|
171
178
|
true
|
|
@@ -181,8 +188,8 @@ module ActiveMatrix
|
|
|
181
188
|
logger.info "Resuming agent: #{agent.name}"
|
|
182
189
|
|
|
183
190
|
agent.resume!
|
|
184
|
-
client = entry[:instance]
|
|
185
|
-
client
|
|
191
|
+
client = entry[:instance]&.client
|
|
192
|
+
client&.start_listener
|
|
186
193
|
agent.connection_established!
|
|
187
194
|
|
|
188
195
|
true
|
|
@@ -193,41 +200,79 @@ module ActiveMatrix
|
|
|
193
200
|
{
|
|
194
201
|
running: @registry.count,
|
|
195
202
|
agents: @registry.health_status,
|
|
196
|
-
monitor_active: @
|
|
197
|
-
shutdown:
|
|
203
|
+
monitor_active: @monitor_task&.alive? || false,
|
|
204
|
+
shutdown: !@running
|
|
198
205
|
}
|
|
199
206
|
end
|
|
200
207
|
|
|
208
|
+
# Check if currently running
|
|
209
|
+
def running?
|
|
210
|
+
@running
|
|
211
|
+
end
|
|
212
|
+
|
|
201
213
|
private
|
|
202
214
|
|
|
203
|
-
def
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
215
|
+
def run_agent(agent)
|
|
216
|
+
Telemetry.trace('agent.run', attributes: agent_attributes(agent)) do |span|
|
|
217
|
+
# Create client and bot instance
|
|
218
|
+
client = create_client_for_agent(agent)
|
|
219
|
+
bot_class = agent.bot_class.constantize
|
|
220
|
+
bot_instance = bot_class.new(client)
|
|
221
|
+
|
|
222
|
+
# Register the agent
|
|
223
|
+
@registry.register(agent, bot_instance)
|
|
224
|
+
|
|
225
|
+
# Authenticate if needed
|
|
226
|
+
if agent.access_token.present?
|
|
227
|
+
client.access_token = agent.access_token
|
|
228
|
+
else
|
|
229
|
+
Telemetry.trace('agent.login', attributes: agent_attributes(agent)) do
|
|
230
|
+
client.login(agent.username, agent.password)
|
|
231
|
+
agent.update(access_token: client.access_token)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Restore sync token if available
|
|
236
|
+
client.sync_token = agent.last_sync_token if agent.last_sync_token.present?
|
|
237
|
+
|
|
238
|
+
# Mark as online
|
|
239
|
+
agent.connection_established!
|
|
240
|
+
span&.add_event('agent.connected')
|
|
241
|
+
|
|
242
|
+
# Run the sync loop (blocks until stopped)
|
|
243
|
+
client.listen_forever
|
|
211
244
|
end
|
|
245
|
+
rescue Async::Stop
|
|
246
|
+
logger.info "Agent #{agent.name} stopping gracefully"
|
|
247
|
+
rescue StandardError => e
|
|
248
|
+
Telemetry.record_exception(e, attributes: agent_attributes(agent))
|
|
249
|
+
logger.error "Error in agent #{agent.name}: #{e.message}"
|
|
250
|
+
logger.error e.backtrace.first(10).join("\n")
|
|
251
|
+
agent.encounter_error!
|
|
252
|
+
raise
|
|
253
|
+
ensure
|
|
254
|
+
@registry.unregister(agent)
|
|
255
|
+
agent.disconnect! if agent.may_disconnect?
|
|
212
256
|
end
|
|
213
257
|
|
|
214
|
-
def
|
|
215
|
-
|
|
258
|
+
def create_client_for_agent(agent)
|
|
259
|
+
ClientPool.instance.get_client(agent.homeserver)
|
|
260
|
+
end
|
|
216
261
|
|
|
217
|
-
|
|
218
|
-
|
|
262
|
+
def start_monitor_task
|
|
263
|
+
return if @monitor_task&.alive?
|
|
219
264
|
|
|
220
|
-
|
|
221
|
-
break if @shutdown
|
|
265
|
+
health_interval = config.agent_health_check_interval || 30
|
|
222
266
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
cleanup_stale_data
|
|
226
|
-
rescue StandardError => e
|
|
227
|
-
logger.error "Monitor thread error: #{e.message}"
|
|
228
|
-
end
|
|
267
|
+
@monitor_task = Async(transient: true) do |task|
|
|
268
|
+
task.annotate 'agent-monitor'
|
|
229
269
|
|
|
230
|
-
|
|
270
|
+
loop do
|
|
271
|
+
sleep(health_interval)
|
|
272
|
+
check_agent_health
|
|
273
|
+
cleanup_stale_data
|
|
274
|
+
rescue StandardError => e
|
|
275
|
+
logger.error "Monitor task error: #{e.message}"
|
|
231
276
|
end
|
|
232
277
|
end
|
|
233
278
|
end
|
|
@@ -235,41 +280,35 @@ module ActiveMatrix
|
|
|
235
280
|
def check_agent_health
|
|
236
281
|
@registry.find_each do |entry|
|
|
237
282
|
agent = entry[:record]
|
|
238
|
-
|
|
283
|
+
task = entry[:task]
|
|
239
284
|
|
|
240
|
-
# Check if
|
|
241
|
-
unless
|
|
242
|
-
logger.warn "Agent #{agent.name}
|
|
285
|
+
# Check if task is alive
|
|
286
|
+
unless task&.alive?
|
|
287
|
+
logger.warn "Agent #{agent.name} task died, restarting..."
|
|
243
288
|
@registry.unregister(agent)
|
|
244
289
|
agent.encounter_error!
|
|
245
|
-
start_agent(agent)
|
|
290
|
+
start_agent(agent) if @running
|
|
246
291
|
next
|
|
247
292
|
end
|
|
248
293
|
|
|
249
294
|
# Check last activity
|
|
250
|
-
if agent.last_active_at && agent.last_active_at < 5.minutes.ago
|
|
251
|
-
logger.warn "Agent #{agent.name} seems inactive"
|
|
252
|
-
# Could implement additional health checks here
|
|
253
|
-
end
|
|
295
|
+
logger.warn "Agent #{agent.name} seems inactive" if agent.last_active_at && agent.last_active_at < 5.minutes.ago
|
|
254
296
|
end
|
|
255
297
|
end
|
|
256
298
|
|
|
257
299
|
def cleanup_stale_data
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
# Clean up expired memories
|
|
262
|
-
AgentMemory.cleanup_expired! if defined?(AgentMemory)
|
|
263
|
-
GlobalMemory.cleanup_expired! if defined?(GlobalMemory)
|
|
300
|
+
ActiveMatrix::ChatSession.cleanup_stale!
|
|
301
|
+
ActiveMatrix::AgentStore.cleanup_expired!
|
|
302
|
+
ActiveMatrix::KnowledgeBase.cleanup_expired!
|
|
264
303
|
end
|
|
265
304
|
|
|
266
|
-
def
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
305
|
+
def agent_attributes(agent)
|
|
306
|
+
OTEL_ATTRS.merge(
|
|
307
|
+
'agent.id' => agent.id,
|
|
308
|
+
'agent.name' => agent.name,
|
|
309
|
+
'agent.homeserver' => agent.homeserver,
|
|
310
|
+
'agent.bot_class' => agent.bot_class
|
|
311
|
+
)
|
|
273
312
|
end
|
|
274
313
|
end
|
|
275
314
|
end
|
|
@@ -11,32 +11,33 @@ module ActiveMatrix
|
|
|
11
11
|
|
|
12
12
|
def initialize
|
|
13
13
|
@agents = Concurrent::Hash.new
|
|
14
|
-
@mutex = Mutex.new
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
# Register a running agent
|
|
18
17
|
def register(agent_record, bot_instance)
|
|
19
|
-
@
|
|
20
|
-
raise AgentAlreadyRunningError, "Agent #{agent_record.name} is already running" if @agents.key?(agent_record.id)
|
|
21
|
-
|
|
22
|
-
@agents[agent_record.id] = {
|
|
23
|
-
record: agent_record,
|
|
24
|
-
instance: bot_instance,
|
|
25
|
-
thread: Thread.current,
|
|
26
|
-
started_at: Time.current
|
|
27
|
-
}
|
|
18
|
+
raise AgentAlreadyRunningError, "Agent #{agent_record.name} is already running" if @agents.key?(agent_record.id)
|
|
28
19
|
|
|
29
|
-
|
|
30
|
-
|
|
20
|
+
@agents[agent_record.id] = {
|
|
21
|
+
record: agent_record,
|
|
22
|
+
instance: bot_instance,
|
|
23
|
+
task: nil,
|
|
24
|
+
started_at: Time.current
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
logger.info "Registered agent: #{agent_record.name} (#{agent_record.id})"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Register an async task for an agent
|
|
31
|
+
def register_task(agent_record, task)
|
|
32
|
+
entry = @agents[agent_record.id]
|
|
33
|
+
entry[:task] = task if entry
|
|
31
34
|
end
|
|
32
35
|
|
|
33
36
|
# Unregister an agent
|
|
34
37
|
def unregister(agent_record)
|
|
35
|
-
@
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
entry
|
|
39
|
-
end
|
|
38
|
+
entry = @agents.delete(agent_record.id)
|
|
39
|
+
logger.info "Unregistered agent: #{agent_record.name} (#{agent_record.id})" if entry
|
|
40
|
+
entry
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
# Get a running agent by ID
|
|
@@ -123,9 +124,7 @@ module ActiveMatrix
|
|
|
123
124
|
|
|
124
125
|
# Clear all agents (used for testing)
|
|
125
126
|
def clear!
|
|
126
|
-
@
|
|
127
|
-
@agents.clear
|
|
128
|
-
end
|
|
127
|
+
@agents.clear
|
|
129
128
|
end
|
|
130
129
|
|
|
131
130
|
# Get health status of all agents
|
|
@@ -135,13 +134,18 @@ module ActiveMatrix
|
|
|
135
134
|
id: id,
|
|
136
135
|
name: entry[:record].name,
|
|
137
136
|
state: entry[:record].state,
|
|
138
|
-
|
|
137
|
+
task_alive: entry[:task]&.alive?,
|
|
139
138
|
uptime: Time.current - entry[:started_at],
|
|
140
139
|
last_active: entry[:record].last_active_at
|
|
141
140
|
}
|
|
142
141
|
end
|
|
143
142
|
end
|
|
144
143
|
|
|
144
|
+
# Iterate through all agents
|
|
145
|
+
def find_each(&)
|
|
146
|
+
@agents.values.each(&)
|
|
147
|
+
end
|
|
148
|
+
|
|
145
149
|
private
|
|
146
150
|
|
|
147
151
|
def by_name_pattern(pattern)
|