Module: PWN::AI::Agent::Loop
- Defined in:
- lib/pwn/ai/agent/loop.rb
Overview
The agent conversation loop:
build system prompt → call LLM with tools → if tool_calls: dispatch,
append role:'tool' results, loop → else: return text.
This replaces the regex-ReAct in PWN::Plugins::REPL :pwn_ai_hook with native function-calling. State (memory, skills, sessions) is all externalised — Loop.run is stateless aside from the messages array it builds.
Constant Summary collapse
- DEFAULT_MAX_ITERS =
25- ENGINE_MODS =
{ openai: 'PWN::AI::OpenAI', grok: 'PWN::AI::Grok', ollama: 'PWN::AI::Ollama', anthropic: 'PWN::AI::Anthropic', gemini: 'PWN::AI::Gemini' }.freeze
Class Method Summary collapse
-
.authors ⇒ Object
- Author(s)
-
0day Inc.
-
.call_engine(opts = {}) ⇒ Object
- Supported Method Parameters
-
msg = PWN::AI::Agent::Loop.call_engine( messages: ‘required - OpenAI-format messages array’, tools: ‘optional - OpenAI tools array’ ).
-
.help ⇒ Object
Display Usage for this Module.
-
.normalise_openai(opts = {}) ⇒ Object
- Supported Method Parameters
-
msg = PWN::AI::Agent::Loop.normalise_openai( response: ‘required - raw chat_raw response Hash from any provider’ ).
-
.run(opts = {}) ⇒ Object
- Supported Method Parameters
-
final = PWN::AI::Agent::Loop.run( user_text: ‘required - what the human typed’, session_id: ‘optional - PWN::Sessions id (transcript is appended to it)’, enabled_toolsets: ‘optional - subset of Registry.toolsets, or nil for all’, on_tool: ‘optional - ->(name, args, result) callback for live UI’ ).
Class Method Details
.authors ⇒ Object
- Author(s)
-
0day Inc. <support@0dayinc.com>
176 177 178 |
# File 'lib/pwn/ai/agent/loop.rb', line 176 public_class_method def self. "AUTHOR(S):\n 0day Inc. <support@0dayinc.com>\n" end |
.call_engine(opts = {}) ⇒ Object
- Supported Method Parameters
-
msg = PWN::AI::Agent::Loop.call_engine(
messages: 'required - OpenAI-format messages array', tools: 'optional - OpenAI tools array')
Returns a normalised assistant message hash:
{ role: 'assistant', content: String|nil, tool_calls: [ {id:, type:'function', function:{name:, arguments:}} ], _native_content: <provider raw> (when adapter needs round-trip) }
95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/pwn/ai/agent/loop.rb', line 95 public_class_method def self.call_engine(opts = {}) = opts[:messages] tools = opts[:tools] engine = (PWN::Env.dig(:ai, :active) if defined?(PWN::Env)).to_s.downcase.to_sym engine = :openai if engine == :'' mod_name = ENGINE_MODS[engine] raise "ERROR: Unsupported AI engine for agent loop: #{engine}" unless mod_name mod = Object.const_get(mod_name) if mod.respond_to?(:chat_raw) normalise_openai(response: mod.chat_raw(messages: , tools: tools, spinner: true)) else degrade_text_only(mod: mod, messages: ) end end |
.help ⇒ Object
Display Usage for this Module
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/pwn/ai/agent/loop.rb', line 182 public_class_method def self.help puts <<~USAGE USAGE: final = PWN::AI::Agent::Loop.run( user_text: 'what does `id` return on this host?', session_id: PWN::Sessions.create[:id], enabled_toolsets: %w[terminal pwn memory skills], on_tool: ->(name, args, result) { puts "→ \#{name}: \#{result[0,80]}" } ) Supported engines: #{ENGINE_MODS.keys.join(', ')} Set PWN::Env[:ai][:active] to choose; PWN::Env[:ai][:agent][:max_iters] to bound. #{self}.authors USAGE end |
.normalise_openai(opts = {}) ⇒ Object
- Supported Method Parameters
-
msg = PWN::AI::Agent::Loop.normalise_openai(
response: 'required - raw chat_raw response Hash from any provider')
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/pwn/ai/agent/loop.rb', line 118 public_class_method def self.normalise_openai(opts = {}) resp = opts[:response] return nil unless resp.is_a?(Hash) msg = resp.dig(:choices, 0, :message) || resp[:assistant_message] return nil unless msg out = { role: 'assistant', content: msg[:content], tool_calls: Array(msg[:tool_calls]).map do |tc| { id: tc[:id], type: 'function', function: { name: tc.dig(:function, :name) || tc[:name], arguments: tc.dig(:function, :arguments) || tc[:arguments] } } end } # Preserve provider-native content blocks so chat_raw can round-trip # them exactly on the next iteration (e.g. Anthropic requires the # original tool_use block to precede a tool_result). out[:_native_content] = msg[:_native_content] if msg[:_native_content] out end |
.run(opts = {}) ⇒ Object
- Supported Method Parameters
-
final = PWN::AI::Agent::Loop.run(
user_text: 'required - what the human typed', session_id: 'optional - PWN::Sessions id (transcript is appended to it)', enabled_toolsets: 'optional - subset of Registry.toolsets, or nil for all', on_tool: 'optional - ->(name, args, result) callback for live UI')
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/pwn/ai/agent/loop.rb', line 36 public_class_method def self.run(opts = {}) user_text = opts[:user_text].to_s session_id = opts[:session_id] on_tool = opts[:on_tool] Registry.discover tools = Registry.definitions(enabled: opts[:enabled_toolsets]) = [ { role: 'system', content: PromptBuilder.build(session_id: session_id) }, { role: 'user', content: user_text } ] append_session(session_id: session_id, role: 'user', content: user_text) max_iters.times do |i| msg = call_engine(messages: , tools: tools) return '[pwn-ai] engine returned no message' if msg.nil? << msg calls = Array(msg[:tool_calls]) if calls.empty? text = msg[:content].to_s append_session(session_id: session_id, role: 'assistant', content: text) return text end calls.each do |tc| name = tc.dig(:function, :name).to_s entry = Registry.lookup(name: name) raw = Dispatch.call(tool_call: tc) result = Result.condition(content: raw, entry: entry) on_tool&.call(name, tc.dig(:function, :arguments), result) << { role: 'tool', tool_call_id: tc[:id] || tc['id'] || "call_#{i}", name: name, content: result } append_session(session_id: session_id, role: 'tool', content: "#{name} → #{result[0, 400]}") end end '[pwn-ai] iteration budget exhausted' end |