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 =
777
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

Class Method Details

.authorsObject

Author(s)

0day Inc. support@0dayinc.com



226
227
228
# File 'lib/pwn/ai/agent/loop.rb', line 226

public_class_method def self.authors
  "AUTHOR(S):\n  0day Inc. <support@0dayinc.com>\n"
end

.helpObject

Display Usage for this Module



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/pwn/ai/agent/loop.rb', line 232

public_class_method def self.help
  puts <<~USAGE
    USAGE:
      final = PWN::AI::Agent::Loop.run(
        request: '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,1_024]}" },
        system_role_content: 'You are a helpful assistant that can call tools to answer questions.'
      )

      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

.run(opts = {}) ⇒ Object

Supported Method Parameters

final = PWN::AI::Agent::Loop.run( request: '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', system_role_content: 'optional - override default system prompt (built from session_id if not provided)' )



167
168
169
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
# File 'lib/pwn/ai/agent/loop.rb', line 167

public_class_method def self.run(opts = {})
  request = opts[:request].to_s
  session_id = opts[:session_id]
  on_tool = opts[:on_tool]
  system_role_content = opts[:system_role_content] ||= PWN::AI::Agent::PromptBuilder.build(session_id: session_id)

  Registry.discover
  expose_current_session(session_id: session_id)

  tools    = Registry.definitions(enabled: opts[:enabled_toolsets])
  messages = [
    { role: 'system', content: system_role_content },
    { role: 'user',   content: request }
  ]
  append_session(session_id: session_id, role: 'user', content: request)

  max_iters.times do |i|
    msg = call_engine(messages: messages, tools: tools)
    return '[pwn-ai] engine returned no message' if msg.nil?

    messages << msg
    calls = Array(msg[:tool_calls])

    if calls.empty?
      text = msg[:content].to_s
      append_session(session_id: session_id, role: 'assistant', content: text)
      Learning.auto_reflect(session_id: session_id, request: request, final: text) if defined?(Learning)
      return text
    end

    calls.each do |tc|
      name    = tc.dig(:function, :name).to_s
      entry   = Registry.lookup(name: name)
      started = Time.now
      raw     = Dispatch.call(tool_call: tc)
      record_metrics(name: name, started: started, raw: raw)
      result = Result.condition(content: raw, entry: entry)

      on_tool&.call(name, tc.dig(:function, :arguments), result)

      messages << {
        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, 1_024]}"
      )
    end
  end

  '[pwn-ai] iteration budget exhausted'
end