fantasy-cli 1.2.14 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5adcd4b07f5da2256106aa2df3bc5ad67ab64d3b62d69dbfb316edc266bf90be
4
- data.tar.gz: 2115dd7040fff7fe0fcd20bda97b6e28b7462bc06b3e77bf4e3f738870b5e558
3
+ metadata.gz: 44d592c83d03126aab712e4f0ffe144439ffde20bda9798974d6d82073222642
4
+ data.tar.gz: 77b9490266d6e8cec2aeda116b9cc6fb68e080d76294db8a9bfa880e0204617a
5
5
  SHA512:
6
- metadata.gz: 85abb14e63e9ae8c6aec9ea4e84995c4c9d26f2410f92630fb0335fa5b40d5e325b4530415b2ed3e983d54cf19b0376160eb820efab5c3e3907e367d3a466f33
7
- data.tar.gz: 78e547cdc205b0638de16917d78fd6499060a73095ac4d3a35bb12cce1ec049c75e279da8f8d052b59961e75bb7dd232c934679bed0d401847edb74a05642b81
6
+ metadata.gz: ebfee63b7e36a8502decd98966def7d40371e6109d2e8f0c2b36e7095fe6998d7502543c842c42bbf76a28f379e9042aba69bf408a8a4f2e7c22e643f3d0db1e
7
+ data.tar.gz: f4d69773cc1d1c06bd31b86f3b87d41c5b4ca48cf3bcebed990c40f02ab96c2c0b9cece32c7226d0373f7727f60cfa313c3e55bc2db6792b4de68f8201d21f01
data/README.md CHANGED
@@ -17,12 +17,19 @@
17
17
  |---------|-----------|
18
18
  | 🤖 **AI Chat** | REPL interativo com suporte a múltiplos providers (Anthropic, OpenAI, Ollama, LM Studio, OpenRouter) |
19
19
  | 📁 **Referências @** | Injete arquivos (`@arquivo.rb`) e diretórios (`@pasta/`) diretamente no contexto da conversa |
20
- | ⌨️ **Comandos /** | `/help`, `/model`, `/cost`, `/clear`, `/export` e mais |
20
+
21
+ | ⌨️ **Comandos /** | `/help`, `/model`, `/cost`, `/clear`, `/export`, `/save`, `/restore`, `/tab`, `/plugin`, `/lsp` e mais |
21
22
  | 🔮 **Autocomplete** | Sugestões em tempo real para referências de arquivos |
22
23
  | 📊 **Git Context** | Branch, status, arquivos modificados e ahead/behind integrados automaticamente |
23
24
  | 💰 **Cost Tracking** | Monitoramento de tokens e custo em tempo real com alertas de orçamento |
24
25
  | 🏗️ **Project Management** | State, Phase, Roadmap, Milestones, Workstreams |
25
26
  | 📝 **Templates** | Geração automática de docs (SUMMARY.md, PLAN.md, VERIFICATION.md) |
27
+ | 🐝 **Multi-Agent Swarm** | Sistema multi-agente com orquestração automática (v1.3.0) |
28
+ | 💾 **Session Persistence** | Save/restore de sessões, checkpoints, auto-save (v1.3.0) |
29
+ | 📑 **Tabs & Panes** | Múltiplas abas independentes no TUI (v1.3.0) |
30
+ | ✨ **Advanced UI** | Animações, transições, mouse support, efeitos visuais (v1.3.0) |
31
+ | 🔌 **Plugin System** | Hot-reload, hooks, sandbox, API para plugins (v1.3.0) |
32
+ | 🔧 **LSP Integration** | Language Server Protocol para Ruby, Python, JS/TS, Go, Rust (v1.3.0) |
26
33
 
27
34
  ---
28
35
 
@@ -0,0 +1,260 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'json'
5
+
6
+ module Gsd
7
+ module Agents
8
+ # Communication - Sistema de mensagens entre agents
9
+ #
10
+ # Implementa:
11
+ # - Message bus para comunicação assíncrona
12
+ # - Event system para notificações
13
+ # - Message persistence (opcional)
14
+ # - Pub/sub patterns
15
+ class Communication
16
+ attr_reader :channels, :subscriptions, :message_log
17
+
18
+ def initialize(persist_messages: false)
19
+ @channels = {} # channel_name => [messages]
20
+ @subscriptions = {} # channel_name => [agent_ids]
21
+ @message_log = [] # Log de todas as mensagens (se persist=true)
22
+ @persist_messages = persist_messages
23
+ @mutex = Mutex.new
24
+ end
25
+
26
+ # Cria um canal de comunicação
27
+ #
28
+ # @param name [String] Nome do canal
29
+ def create_channel(name)
30
+ @mutex.synchronize do
31
+ @channels[name] ||= []
32
+ @subscriptions[name] ||= []
33
+ end
34
+ end
35
+
36
+ # Publica mensagem em um canal
37
+ #
38
+ # @param channel [String] Nome do canal
39
+ # @param message [Hash] Conteúdo da mensagem
40
+ # @param sender [String] ID do agente remetente
41
+ def publish(channel:, message:, sender:)
42
+ @mutex.synchronize do
43
+ create_channel(channel) unless @channels.key?(channel)
44
+
45
+ envelope = Message.new(
46
+ channel: channel,
47
+ sender: sender,
48
+ content: message,
49
+ timestamp: Time.now
50
+ )
51
+
52
+ @channels[channel] << envelope
53
+ @message_log << envelope if @persist_messages
54
+
55
+ # Notifica subscribers
56
+ notify_subscribers(channel, envelope)
57
+ end
58
+ end
59
+
60
+ # Inscreve um agente em um canal
61
+ #
62
+ # @param agent_id [String] ID do agente
63
+ # @param channel [String] Nome do canal
64
+ def subscribe(agent_id:, channel:)
65
+ @mutex.synchronize do
66
+ create_channel(channel) unless @channels.key?(channel)
67
+ @subscriptions[channel] << agent_id unless @subscriptions[channel].include?(agent_id)
68
+ end
69
+ end
70
+
71
+ # Cancela inscrição de um agente
72
+ #
73
+ # @param agent_id [String] ID do agente
74
+ # @param channel [String] Nome do canal
75
+ def unsubscribe(agent_id:, channel:)
76
+ @mutex.synchronize do
77
+ @subscriptions[channel]&.delete(agent_id)
78
+ end
79
+ end
80
+
81
+ # Obtém mensagens não lidas de um canal para um agente
82
+ #
83
+ # @param agent_id [String] ID do agente
84
+ # @param channel [String] Nome do canal
85
+ # @param since [Time, nil] Timestamp para filtrar mensagens
86
+ # @return [Array<Message>] Mensagens para o agente
87
+ def receive(agent_id:, channel:, since: nil)
88
+ @mutex.synchronize do
89
+ return [] unless @channels.key?(channel)
90
+
91
+ messages = @channels[channel]
92
+ messages = messages.select { |m| m.timestamp > since } if since
93
+
94
+ # Filtra mensagens destinadas a este agente ou broadcast
95
+ messages.select do |m|
96
+ m.recipients.nil? || m.recipients.include?(agent_id) || m.sender != agent_id
97
+ end
98
+ end
99
+ end
100
+
101
+ # Envia mensagem direta (DM) para agente específico
102
+ #
103
+ # @param to [String] ID do destinatário
104
+ # @param from [String] ID do remetente
105
+ # @param content [Hash] Conteúdo
106
+ def direct_message(to:, from:, content:)
107
+ channel = "dm:#{[to, from].sort.join(':')}"
108
+
109
+ @mutex.synchronize do
110
+ create_channel(channel)
111
+
112
+ envelope = Message.new(
113
+ channel: channel,
114
+ sender: from,
115
+ recipients: [to],
116
+ content: content,
117
+ timestamp: Time.now
118
+ )
119
+
120
+ @channels[channel] << envelope
121
+ @message_log << envelope if @persist_messages
122
+ end
123
+ end
124
+
125
+ # Broadcast para todos agents no canal
126
+ #
127
+ # @param channel [String] Nome do canal
128
+ # @param content [Hash] Conteúdo
129
+ # @param sender [String] ID do remetente
130
+ def broadcast(channel:, content:, sender:)
131
+ publish(channel: channel, message: content, sender: sender)
132
+ end
133
+
134
+ # Obtém histórico de mensagens
135
+ #
136
+ # @param channel [String, nil] Filtrar por canal (nil = todos)
137
+ # @param limit [Integer] Limite de mensagens
138
+ # @return [Array<Message>] Histórico
139
+ def history(channel: nil, limit: 100)
140
+ @mutex.synchronize do
141
+ messages = if channel
142
+ @channels[channel] || []
143
+ else
144
+ @message_log
145
+ end
146
+
147
+ messages.last(limit)
148
+ end
149
+ end
150
+
151
+ # Limpa mensagens antigas de um canal
152
+ #
153
+ # @param channel [String] Nome do canal
154
+ # @param older_than [Time] Remover mensagens mais antigas que
155
+ def cleanup(channel:, older_than:)
156
+ @mutex.synchronize do
157
+ return unless @channels.key?(channel)
158
+
159
+ @channels[channel] = @channels[channel].select { |m| m.timestamp > older_than }
160
+ end
161
+ end
162
+
163
+ # Estatísticas de comunicação
164
+ #
165
+ # @return [Hash] Estatísticas
166
+ def stats
167
+ @mutex.synchronize do
168
+ {
169
+ channels: @channels.count,
170
+ total_messages: @channels.values.sum(&:count),
171
+ subscriptions: @subscriptions.values.sum(&:count),
172
+ persisted_messages: @message_log.count
173
+ }
174
+ end
175
+ end
176
+
177
+ private
178
+
179
+ def notify_subscribers(channel, message)
180
+ # Callback para notificar subscribers externos
181
+ # Implementação real usaria callbacks ou event system
182
+ end
183
+ end
184
+
185
+ # Representação de uma mensagem
186
+ class Message
187
+ attr_reader :id, :channel, :sender, :recipients, :content, :timestamp
188
+
189
+ def initialize(channel:, sender:, content:, timestamp:, recipients: nil)
190
+ @id = SecureRandom.uuid
191
+ @channel = channel
192
+ @sender = sender
193
+ @recipients = recipients
194
+ @content = content
195
+ @timestamp = timestamp
196
+ end
197
+
198
+ def to_h
199
+ {
200
+ id: @id,
201
+ channel: @channel,
202
+ sender: @sender,
203
+ recipients: @recipients,
204
+ content: @content,
205
+ timestamp: @timestamp.to_s
206
+ }
207
+ end
208
+
209
+ def to_json(*args)
210
+ to_h.to_json(*args)
211
+ end
212
+
213
+ def direct?
214
+ @recipients.present?
215
+ end
216
+
217
+ def broadcast?
218
+ @recipients.nil?
219
+ end
220
+ end
221
+
222
+ # Event System para notificações assíncronas
223
+ class EventBus
224
+ def initialize
225
+ @handlers = {}
226
+ @mutex = Mutex.new
227
+ end
228
+
229
+ # Registra handler para um evento
230
+ #
231
+ # @param event [Symbol] Nome do evento
232
+ # @param handler [Proc] Handler
233
+ def on(event, &handler)
234
+ @mutex.synchronize do
235
+ @handlers[event] ||= []
236
+ @handlers[event] << handler
237
+ end
238
+ end
239
+
240
+ # Emite evento
241
+ #
242
+ # @param event [Symbol] Nome do evento
243
+ # @param data [Hash] Dados do evento
244
+ def emit(event, **data)
245
+ handlers = @mutex.synchronize { @handlers[event]&.dup || [] }
246
+
247
+ handlers.each do |handler|
248
+ Thread.new { handler.call(data) }
249
+ end
250
+ end
251
+
252
+ # Remove todos handlers de um evento
253
+ #
254
+ # @param event [Symbol] Nome do evento
255
+ def off(event)
256
+ @mutex.synchronize { @handlers.delete(event) }
257
+ end
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,341 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'observer'
5
+
6
+ module Gsd
7
+ module Agents
8
+ # AgentSwarm - Orquestração multi-agente avançada
9
+ #
10
+ # Gerencia múltiplos agents simultâneos com:
11
+ # - Spawn/monitoramento de agents
12
+ # - Roteamento inteligente de tarefas
13
+ # - Comunicação entre agents
14
+ # - Execução paralela
15
+ # - Auto-recovery
16
+ class Swarm
17
+ include Observable
18
+
19
+ attr_reader :agents, :active_agents, :task_queue, :metrics
20
+
21
+ def initialize(max_concurrent: 5, auto_recovery: true)
22
+ @agents = {} # Hash de agent_id => agent
23
+ @active_agents = [] # Lista de agents ativos
24
+ @task_queue = [] # Fila de tarefas pendentes
25
+ @max_concurrent = max_concurrent
26
+ @auto_recovery = auto_recovery
27
+ @metrics = {
28
+ tasks_completed: 0,
29
+ tasks_failed: 0,
30
+ spawn_count: 0,
31
+ total_runtime: 0.0
32
+ }
33
+ @running = false
34
+ @mutex = Mutex.new
35
+ end
36
+
37
+ # Inicia o swarm (loop de orquestração)
38
+ def start
39
+ @running = true
40
+ Thread.new { orchestration_loop }
41
+ end
42
+
43
+ # Para o swarm
44
+ def stop
45
+ @running = false
46
+ end
47
+
48
+ # Cria um novo agent no swarm
49
+ #
50
+ # @param type [Symbol] Tipo do agente (:code, :plan, :debug, etc)
51
+ # @param config [Hash] Configuração do agente
52
+ # @return [String] ID do agente criado
53
+ def spawn(type:, config: {})
54
+ agent_id = generate_agent_id
55
+ agent = Agent.new(
56
+ id: agent_id,
57
+ type: type,
58
+ config: config,
59
+ swarm: self
60
+ )
61
+
62
+ @mutex.synchronize do
63
+ @agents[agent_id] = agent
64
+ @active_agents << agent_id
65
+ @metrics[:spawn_count] += 1
66
+ end
67
+
68
+ # Notifica observadores
69
+ changed
70
+ notify_observers(:agent_spawned, agent_id, type)
71
+
72
+ agent_id
73
+ end
74
+
75
+ # Encerra um agente específico
76
+ #
77
+ # @param agent_id [String] ID do agente
78
+ def terminate(agent_id)
79
+ @mutex.synchronize do
80
+ if agent = @agents.delete(agent_id)
81
+ @active_agents.delete(agent_id)
82
+ agent.stop
83
+ end
84
+ end
85
+
86
+ changed
87
+ notify_observers(:agent_terminated, agent_id)
88
+ end
89
+
90
+ # Roteia uma tarefa para o agente mais adequado
91
+ #
92
+ # @param task [Hash] Descrição da tarefa
93
+ # @return [String] ID do agente que recebeu a tarefa
94
+ def route_task(task)
95
+ router = Router.new(@agents.values)
96
+ best_agent = router.select_agent_for(task)
97
+
98
+ if best_agent
99
+ best_agent.assign_task(task)
100
+ changed
101
+ notify_observers(:task_routed, task, best_agent.id)
102
+ best_agent.id
103
+ else
104
+ # Enfileira se nenhum agente disponível
105
+ @mutex.synchronize { @task_queue << task }
106
+ changed
107
+ notify_observers(:task_queued, task)
108
+ nil
109
+ end
110
+ end
111
+
112
+ # Envia mensagem de um agente para outro
113
+ #
114
+ # @param from [String] ID do agente remetente
115
+ # @param to [String] ID do agente destinatário
116
+ # @param message [Hash] Mensagem
117
+ def send_message(from:, to:, message:)
118
+ return unless target = @agents[to]
119
+
120
+ envelope = {
121
+ from: from,
122
+ to: to,
123
+ message: message,
124
+ timestamp: Time.now,
125
+ id: SecureRandom.uuid
126
+ }
127
+
128
+ target.receive_message(envelope)
129
+
130
+ changed
131
+ notify_observers(:message_sent, envelope)
132
+ end
133
+
134
+ # Broadcast mensagem para todos agents de um tipo
135
+ #
136
+ # @param from [String] ID do agente remetente
137
+ # @param type [Symbol] Tipo de agente destinatário
138
+ # @param message [Hash] Mensagem
139
+ def broadcast(from:, type:, message:)
140
+ targets = @agents.values.select { |a| a.type == type }
141
+
142
+ targets.each do |agent|
143
+ send_message(from: from, to: agent.id, message: message)
144
+ end
145
+ end
146
+
147
+ # Obtém estatísticas do swarm
148
+ #
149
+ # @return [Hash] Métricas atuais
150
+ def stats
151
+ @mutex.synchronize do
152
+ {
153
+ total_agents: @agents.count,
154
+ active_agents: @active_agents.count,
155
+ queued_tasks: @task_queue.count,
156
+ **@metrics
157
+ }
158
+ end
159
+ end
160
+
161
+ # Lista todos agents de um tipo específico
162
+ #
163
+ # @param type [Symbol, nil] Tipo de agente (nil = todos)
164
+ # @return [Array<Agent>] Lista de agents
165
+ def list(type: nil)
166
+ agents = @agents.values
167
+ agents = agents.select { |a| a.type == type } if type
168
+ agents
169
+ end
170
+
171
+ private
172
+
173
+ def generate_agent_id
174
+ "agent-#{SecureRandom.uuid[0..7]}"
175
+ end
176
+
177
+ def orchestration_loop
178
+ while @running
179
+ @mutex.synchronize do
180
+ # Processa fila de tarefas
181
+ process_task_queue
182
+
183
+ # Monitora agents e faz recovery se necessário
184
+ monitor_agents if @auto_recovery
185
+
186
+ # Atualiza métricas
187
+ update_metrics
188
+ end
189
+
190
+ sleep(0.5) # Intervalo de orquestração
191
+ end
192
+ end
193
+
194
+ def process_task_queue
195
+ return if @task_queue.empty?
196
+ return if @active_agents.count >= @max_concurrent
197
+
198
+ # Pega próxima tarefa da fila
199
+ task = @task_queue.shift
200
+ route_task(task)
201
+ end
202
+
203
+ def monitor_agents
204
+ @active_agents.each do |agent_id|
205
+ agent = @agents[agent_id]
206
+ next unless agent
207
+
208
+ if agent.crashed?
209
+ # Tenta restart
210
+ changed
211
+ notify_observers(:agent_crashed, agent_id)
212
+
213
+ if @auto_recovery
214
+ agent.restart
215
+ changed
216
+ notify_observers(:agent_restarted, agent_id)
217
+ else
218
+ terminate(agent_id)
219
+ end
220
+ end
221
+ end
222
+ end
223
+
224
+ def update_metrics
225
+ @metrics[:total_runtime] += 0.5
226
+ end
227
+ end
228
+
229
+ # Agente individual dentro do swarm
230
+ class Agent
231
+ attr_reader :id, :type, :config, :state, :task_count
232
+
233
+ def initialize(id:, type:, config:, swarm:)
234
+ @id = id
235
+ @type = type
236
+ @config = config
237
+ @swarm = swarm
238
+ @state = :idle
239
+ @task_count = 0
240
+ @current_task = nil
241
+ @message_queue = []
242
+ @mutex = Mutex.new
243
+ end
244
+
245
+ # Atribui uma tarefa ao agente
246
+ def assign_task(task)
247
+ @mutex.synchronize do
248
+ @current_task = task
249
+ @state = :working
250
+ @task_count += 1
251
+ end
252
+
253
+ # Executa em thread separada
254
+ Thread.new { execute_task(task) }
255
+ end
256
+
257
+ # Recebe mensagem de outro agente
258
+ def receive_message(envelope)
259
+ @mutex.synchronize do
260
+ @message_queue << envelope
261
+ end
262
+ end
263
+
264
+ # Obtém mensagens pendentes
265
+ def pending_messages
266
+ @mutex.synchronize do
267
+ messages = @message_queue.dup
268
+ @message_queue.clear
269
+ messages
270
+ end
271
+ end
272
+
273
+ # Para o agente
274
+ def stop
275
+ @mutex.synchronize { @state = :stopped }
276
+ end
277
+
278
+ # Reinicia o agente (recovery)
279
+ def restart
280
+ @mutex.synchronize do
281
+ @state = :idle
282
+ @current_task = nil
283
+ end
284
+ end
285
+
286
+ # Verifica se agente crashou
287
+ def crashed?
288
+ @state == :crashed
289
+ end
290
+
291
+ # Reporta crash (para ser chamado se algo der errado)
292
+ def report_crash(error)
293
+ @mutex.synchronize { @state = :crashed }
294
+ end
295
+
296
+ private
297
+
298
+ def execute_task(task)
299
+ # Implementação simulada - na prática, chamaría o provider de AI
300
+ sleep(rand(1.0..3.0)) # Simula processamento
301
+
302
+ @mutex.synchronize do
303
+ @state = :idle
304
+ @current_task = nil
305
+ end
306
+
307
+ # Notifica swarm que terminou
308
+ @swarm&.changed
309
+ @swarm&.notify_observers(:task_completed, @id, task)
310
+ rescue => e
311
+ report_crash(e)
312
+ end
313
+ end
314
+
315
+ # Router inteligente para distribuir tarefas
316
+ class Router
317
+ def initialize(agents)
318
+ @agents = agents
319
+ end
320
+
321
+ # Seleciona o melhor agente para uma tarefa
322
+ def select_agent_for(task)
323
+ available = @agents.select { |a| a.state == :idle }
324
+ return nil if available.empty?
325
+
326
+ # Roteamento baseado no tipo de tarefa
327
+ case task[:type]
328
+ when :code, :programming, :debug
329
+ available.select { |a| a.type == :code }.first
330
+ when :plan, :architecture, :design
331
+ available.select { |a| a.type == :plan }.first
332
+ when :debug, :error, :fix
333
+ available.select { |a| a.type == :debug }.first
334
+ else
335
+ # Round-robin para tarefas genéricas
336
+ available.min_by(&:task_count)
337
+ end
338
+ end
339
+ end
340
+ end
341
+ end