Module: PWN::AI::Agent::Swarm
- Defined in:
- lib/pwn/ai/agent/swarm.rb
Overview
Native multi-agent orchestration for pwn-ai.
Swarm replaces the legacy pwn-irc mechanism (inspircd + weechat +
PRIVMSG-flattened .chat calls) with first-class sub-agents built on
top of PWN::AI::Agent::Loop.run. Each persona is a full tool-calling
agent — Memory, Skills, Learning, Metrics and Extrospection all
apply — so the self-improvement loop covers the whole swarm.
~/.pwn/agents.yml # persona registry
~/.pwn/swarm/<swarm_id>/bus.jsonl # append-only message bus
~/.pwn/swarm/<swarm_id>/personas.json# persona -> PWN::Sessions id
Cross-session / cross-process communication == another pwn-ai (or a PWN::Cron job) calling Swarm.ask/debate with the same swarm_id and reading the same bus.jsonl. No daemon required.
Constant Summary collapse
- AGENTS_FILE =
File.join(Dir.home, '.pwn', 'agents.yml')
- SWARM_ROOT =
File.join(Dir.home, '.pwn', 'swarm')
- DEFAULT_DEPTH =
3- DEFAULT_ITERS =
25- DEFAULT_TAIL =
12- DEFAULT_TOOLSET =
%w[terminal pwn memory skills sessions learning metrics extrospection].freeze
Class Method Summary collapse
-
.ask(opts = {}) ⇒ Object
- Supported Method Parameters
reply = PWN::AI::Agent::Swarm.ask( name: 'required - persona name from ~/.pwn/agents.yml', request: 'required - what to ask/instruct the persona', swarm_id: 'optional - join an existing swarm (created if omitted)', to: 'optional - addressee recorded on the bus (default :all)', on_tool: 'optional - ->(name, args, result) live-UI callback' ).
-
.authors ⇒ Object
- Author(s)
0day Inc.
-
.broadcast(opts = {}) ⇒ Object
- Supported Method Parameters
result = PWN::AI::Agent::Swarm.broadcast( request: 'required', names: 'optional - default all personas', swarm_id: 'optional' ).
-
.bus_append(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::AI::Agent::Swarm.bus_append( swarm_id: 'required', from: 'required', content: 'required', to: 'optional (default :all)' ).
-
.bus_tail(opts = {}) ⇒ Object
- Supported Method Parameters
msgs = PWN::AI::Agent::Swarm.bus_tail(swarm_id: 'required', limit: 12).
-
.create(opts = {}) ⇒ Object
- Supported Method Parameters
swarm = PWN::AI::Agent::Swarm.create(topic: 'optional').
-
.debate(opts = {}) ⇒ Object
- Supported Method Parameters
result = PWN::AI::Agent::Swarm.debate( names: 'required - Array of persona names, order = speaking order', topic: 'required - opening question / claim', rounds: 'optional - full passes over names (default 2)', swarm_id: 'optional - join an existing swarm', on_tool: 'optional - ->(name, args, result) live-UI callback' ).
-
.help ⇒ Object
Display Usage for this Module.
-
.list ⇒ Object
- Supported Method Parameters
swarms = PWN::AI::Agent::Swarm.list.
-
.personas ⇒ Object
- Supported Method Parameters
personas = PWN::AI::Agent::Swarm.personas.
-
.retire(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::AI::Agent::Swarm.retire(name: 'required - persona name').
-
.spawn(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::AI::Agent::Swarm.spawn( name: 'required - persona name (snake_case)', role: 'required - system_role_content overlay for this persona', toolsets: 'optional - Array of Registry toolset names', engine: 'optional - :openai / :anthropic / :grok / :gemini / :ollama', max_iters: 'optional - per-turn iteration cap for this persona' ).
Class Method Details
.ask(opts = {}) ⇒ Object
- Supported Method Parameters
reply = PWN::AI::Agent::Swarm.ask( name: 'required - persona name from ~/.pwn/agents.yml', request: 'required - what to ask/instruct the persona', swarm_id: 'optional - join an existing swarm (created if omitted)', to: 'optional - addressee recorded on the bus (default :all)', on_tool: 'optional - ->(name, args, result) live-UI callback' )
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/pwn/ai/agent/swarm.rb', line 170 public_class_method def self.ask(opts = {}) name = opts[:name].to_s persona = personas[name.to_sym] raise ArgumentError, "unknown persona: #{name} (see #{AGENTS_FILE})" unless persona sid = opts[:swarm_id] || create(topic: opts[:request].to_s[0, 60])[:swarm_id] depth = Thread.current[:pwn_swarm_depth] || 0 if depth >= max_depth raise "swarm recursion depth #{depth} >= max_depth #{max_depth} " \ '(PWN::Env[:ai][:agent][:max_depth])' end bus_append(swarm_id: sid, from: opts[:from] || caller_label, to: name, content: opts[:request].to_s) session_id = persona_session(swarm_id: sid, name: name) sys = build_persona_prompt(name: name, persona: persona, swarm_id: sid, session_id: session_id) Thread.current[:pwn_swarm_depth] = depth + 1 reply = with_persona_env(persona: persona) do Loop.run( request: opts[:request].to_s, session_id: session_id, enabled_toolsets: persona[:toolsets], system_role_content: sys, on_tool: opts[:on_tool] ) end bus_append(swarm_id: sid, from: name, to: opts[:to] || :all, content: reply) { swarm_id: sid, name: name, session_id: session_id, reply: reply } ensure Thread.current[:pwn_swarm_depth] = depth end |
.authors ⇒ Object
- Author(s)
0day Inc. support@0dayinc.com
390 391 392 |
# File 'lib/pwn/ai/agent/swarm.rb', line 390 public_class_method def self. "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n" end |
.broadcast(opts = {}) ⇒ Object
- Supported Method Parameters
result = PWN::AI::Agent::Swarm.broadcast( request: 'required', names: 'optional - default all personas', swarm_id: 'optional' )
255 256 257 258 259 260 261 262 263 264 265 266 267 268 |
# File 'lib/pwn/ai/agent/swarm.rb', line 255 public_class_method def self.broadcast(opts = {}) req = opts[:request].to_s raise ArgumentError, 'request is required' if req.strip.empty? names = Array(opts[:names]).map(&:to_s) names = personas.keys.map(&:to_s) if names.empty? sid = opts[:swarm_id] || create(topic: req[0, 60])[:swarm_id] replies = names.to_h do |n| [n, ask(name: n, request: req, swarm_id: sid, from: 'broadcast', on_tool: opts[:on_tool])[:reply]] end { swarm_id: sid, replies: replies } end |
.bus_append(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::AI::Agent::Swarm.bus_append( swarm_id: 'required', from: 'required', content: 'required', to: 'optional (default :all)' )
128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/pwn/ai/agent/swarm.rb', line 128 public_class_method def self.bus_append(opts = {}) sid = opts[:swarm_id].to_s raise ArgumentError, 'swarm_id is required' if sid.empty? FileUtils.mkdir_p(File.join(SWARM_ROOT, sid)) entry = { ts: Time.now.utc.iso8601, from: opts[:from].to_s, to: (opts[:to] || :all).to_s, content: opts[:content].to_s } File.open(bus_path(swarm_id: sid), 'a') { |f| f.puts(JSON.generate(entry)) } entry end |
.bus_tail(opts = {}) ⇒ Object
- Supported Method Parameters
msgs = PWN::AI::Agent::Swarm.bus_tail(swarm_id: 'required', limit: 12)
146 147 148 149 150 151 152 153 154 155 |
# File 'lib/pwn/ai/agent/swarm.rb', line 146 public_class_method def self.bus_tail(opts = {}) sid = opts[:swarm_id].to_s limit = (opts[:limit] || DEFAULT_TAIL).to_i bp = bus_path(swarm_id: sid) return [] unless File.exist?(bp) File.readlines(bp).last(limit).map { |l| JSON.parse(l, symbolize_names: true) } rescue StandardError [] end |
.create(opts = {}) ⇒ Object
- Supported Method Parameters
swarm = PWN::AI::Agent::Swarm.create(topic: 'optional')
97 98 99 100 101 102 103 104 |
# File 'lib/pwn/ai/agent/swarm.rb', line 97 public_class_method def self.create(opts = {}) id = "#{Time.now.utc.strftime('%Y%m%d_%H%M%S')}_#{SecureRandom.hex(3)}" dir = File.join(SWARM_ROOT, id) FileUtils.mkdir_p(dir) bus_append(swarm_id: id, from: :system, to: :all, content: "swarm #{id} created: #{opts[:topic] || '(no topic)'}") { swarm_id: id, dir: dir, bus: bus_path(swarm_id: id) } end |
.debate(opts = {}) ⇒ Object
- Supported Method Parameters
result = PWN::AI::Agent::Swarm.debate( names: 'required - Array of persona names, order = speaking order', topic: 'required - opening question / claim', rounds: 'optional - full passes over names (default 2)', swarm_id: 'optional - join an existing swarm', on_tool: 'optional - ->(name, args, result) live-UI callback' )
215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 |
# File 'lib/pwn/ai/agent/swarm.rb', line 215 public_class_method def self.debate(opts = {}) names = Array(opts[:names]).map(&:to_s) raise ArgumentError, 'names must contain at least 2 personas' if names.length < 2 topic = opts[:topic].to_s raise ArgumentError, 'topic is required' if topic.strip.empty? rounds = (opts[:rounds] || 2).to_i sid = opts[:swarm_id] || create(topic: topic)[:swarm_id] last_speaker = 'moderator' last_msg = topic transcript = [] rounds.times do |r| names.each do |n| req = if r.zero? && n == names.first topic else "@#{last_speaker} said:\n#{last_msg}\n\n" \ 'Respond, critique, or advance the objective.' end res = ask(name: n, request: req, swarm_id: sid, from: last_speaker, to: n, on_tool: opts[:on_tool]) transcript << { round: r + 1, name: n, reply: res[:reply] } last_speaker = n last_msg = res[:reply] end end { swarm_id: sid, rounds: rounds, names: names, transcript: transcript, bus: bus_path(swarm_id: sid) } end |
.help ⇒ Object
Display Usage for this Module
396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 |
# File 'lib/pwn/ai/agent/swarm.rb', line 396 public_class_method def self.help puts <<~USAGE USAGE: # Define personas (or edit #{AGENTS_FILE} directly) PWN::AI::Agent::Swarm.spawn( name: 'red', role: 'Offensive researcher. Propose the most likely exploit path...', toolsets: %w[terminal pwn memory extrospection], engine: :anthropic ) # One-shot: ask a persona (creates a swarm if none given) r = PWN::AI::Agent::Swarm.ask(name: 'red', request: 'Enumerate attack surface for target X') # Antagonistic feedback loop d = PWN::AI::Agent::Swarm.debate( names: %w[red blue], topic: 'Is CVE-2026-NNNN exploitable on target X?', rounds: 3 ) puts d[:transcript].map { |t| "\#{t[:name]}: \#{t[:reply][0,200]}" } # Fan-out PWN::AI::Agent::Swarm.broadcast(request: 'Summarise findings so far') # Inspect / resume cross-session PWN::AI::Agent::Swarm.list PWN::AI::Agent::Swarm.bus_tail(swarm_id: d[:swarm_id], limit: 50) Config: PWN::Env[:ai][:agent][:max_depth] # recursion cap (default #{DEFAULT_DEPTH}) persona[:max_iters] # per-turn iteration cap (default #{DEFAULT_ITERS}) persona[:toolsets] # Registry toolset allow-list; omit 'swarm' # to prevent that persona spawning sub-agents. #{self}.authors USAGE end |
.list ⇒ Object
- Supported Method Parameters
swarms = PWN::AI::Agent::Swarm.list
109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/pwn/ai/agent/swarm.rb', line 109 public_class_method def self.list FileUtils.mkdir_p(SWARM_ROOT) Dir.children(SWARM_ROOT).sort.reverse.map do |id| bp = bus_path(swarm_id: id) { swarm_id: id, dir: File.join(SWARM_ROOT, id), messages: File.exist?(bp) ? File.foreach(bp).count : 0, mtime: File.exist?(bp) ? File.mtime(bp).utc.iso8601 : nil } end end |
.personas ⇒ Object
- Supported Method Parameters
personas = PWN::AI::Agent::Swarm.personas
43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
# File 'lib/pwn/ai/agent/swarm.rb', line 43 public_class_method def self.personas return {} unless File.exist?(AGENTS_FILE) raw = YAML.safe_load_file( AGENTS_FILE, permitted_classes: [Symbol], aliases: true, symbolize_names: true ) || {} raw.transform_values { |v| normalize_persona(persona: v) } rescue StandardError => e warn "[pwn-ai/swarm] failed to load #{AGENTS_FILE}: #{e.class}: #{e.}" {} end |
.retire(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::AI::Agent::Swarm.retire(name: 'required - persona name')
82 83 84 85 86 87 88 |
# File 'lib/pwn/ai/agent/swarm.rb', line 82 public_class_method def self.retire(opts = {}) name = opts[:name].to_s all = personas gone = all.delete(name.to_sym) File.write(AGENTS_FILE, YAML.dump(deep_stringify(hash: all))) if gone { name: name, removed: !gone.nil? } end |
.spawn(opts = {}) ⇒ Object
- Supported Method Parameters
PWN::AI::Agent::Swarm.spawn( name: 'required - persona name (snake_case)', role: 'required - system_role_content overlay for this persona', toolsets: 'optional - Array of Registry toolset names', engine: 'optional - :openai / :anthropic / :grok / :gemini / :ollama', max_iters: 'optional - per-turn iteration cap for this persona' )
67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/pwn/ai/agent/swarm.rb', line 67 public_class_method def self.spawn(opts = {}) name = opts[:name].to_s raise ArgumentError, 'name is required' if name.strip.empty? raise ArgumentError, 'role is required' if opts[:role].to_s.strip.empty? all = personas all[name.to_sym] = normalize_persona(persona: opts) FileUtils.mkdir_p(File.dirname(AGENTS_FILE)) File.write(AGENTS_FILE, YAML.dump(deep_stringify(hash: all))) { name: name, persona: all[name.to_sym], file: AGENTS_FILE } end |