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>



196
197
198
# File 'lib/pwn/ai/agent/loop.rb', line 196

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

.helpObject

Display Usage for this Module



202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
# File 'lib/pwn/ai/agent/loop.rb', line 202

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)'

)



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
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
# File 'lib/pwn/ai/agent/loop.rb', line 141

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

  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)
      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)

      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