Module: PWN::AI::Agent::Loop
- Defined in:
- lib/pwn/ai/agent/loop.rb
Overview
The agent conversation loop:
build system prompt
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.
NEGATIVE-FEEDBACK CLOSURE
Loop.run is where "learn from mistakes, don't repeat them" is actually enforced. On EVERY failed dispatch it:
1. Records the (tool, normalised_error) fingerprint into
PWN::AI::Agent::Mistakes with a PERSISTENT cross-session count.
2. Reads that count back and, if it OR the in-turn count reaches
REPEAT_THRESHOLD, prepends a hard "REPEATED FAILURE — change
approach" guard to the tool result the model sees next.
3. Appends Mistakes.correction_hint (seen N
PromptBuilder.mistakes_block re-injects the top open mistakes and top known fixes into the system prompt of every future turn.
COMPLETION
The original request is the completion signal. TaskSummarizer and Policy are advisory (compass / rank). Loop keeps calling CORE_TOOLS until that request is done or a tool returned failure evidence, then stops.
LOCAL-MODEL SCAFFOLDING
When the active engine is :ollama (or the corresponding :agent flags are set) Loop.run additionally:
* threads request
Defined Under Namespace
Classes: Steering
Constant Summary collapse
- DEFAULT_MAX_ITERS =
777- ESCALATE_AFTER_FAILS =
4- BOUNCE_FAIL_KEYS =
%w[ unsatisfied incomplete_final empty_final evidence_final ].freeze
- ENGINE_MODS =
{ openai: 'PWN::AI::OpenAI', grok: 'PWN::AI::Grok', ollama: 'PWN::AI::Ollama', openwebui: 'PWN::AI::OpenWebUI', anthropic: 'PWN::AI::Anthropic', gemini: 'PWN::AI::Gemini' }.freeze
- HOT_WINDOW_SECS =
P17 — true when RECENT unresolved agent_loop / assistant_answer budget fingerprints dominate Mistakes.top. Sliding window + auto-cool so the loop's own exhaust-path Mistakes.record cannot permanently latch hot (scar 8ec3303ed69e self-latch). Do NOT deepen caps; cool the detector.
48 * 3600
- HOT_COOL_MAX_RECENT =
<=1 budget hit in window => cooled (not hot)
1- PARK_COOL_SECS =
P17 rate-based cool/park for permanent budget scars (8ec3303ed69e). Leaves scar open but parks it so it stops dominating Mistakes.top. Resolve is rate-based only (external / after multi-day cool) — never because a guard patch landed. PARK_COOL_SECS (24h) is intentionally shorter than HOT_WINDOW (48h): once hot?=false, a single cooled scar must not keep owning Mistakes.top for another full day.
24 * 3600
- PRIVILEGED_TOOLSETS =
%w[cron swarm].freeze
- SNAPSHOT_STALE_SECS =
6 * 3600
- INCOMPLETE_FINAL_RX =
P28 — incomplete / handoff finals: model emitted text-only before the goal was done ("shall I proceed?", "next step:", "want me to…"). Loop.run treats no-tool_calls as FINAL; this detector lets us refuse that handoff and keep the tool loop alive for multi-step autonomy.
Local/thinking models (gemma/Qwen abliterated etc.) often emit a monologue that NARRATES the next tool ("Wait, let's try hping3…") without producing native tool_calls or shell(...). Treat that as incomplete too so the loop re-pressures tools instead of FINAL.
/ \b(shall\s+i|should\s+i|may\s+i|can\s+i|want\s+me\s+to|do\s+you\s+want\s+me| next\s+single\s+step|next\s+step\s*:|awaiting\s+your\s+(ok|approval|go-ahead|confirmation)| if\s+you(?:'d|\s+would)\s+like\s+me\s+to|say\s+the\s+word|confirm\s+(before|and\s+i)| ready\s+to\s+proceed|ok\s+to\s+(proceed|continue|apply)|proceed\?| continue\?|before\s+i\s+(apply|change|run|continue|proceed)| once\s+you\s+(confirm|approve)|let\s+me\s+know\s+if| i(?:'ll|\s+will)\s+wait\b|waiting\s+for\s+(your\s+)?(go|ok|approval|confirmation) )\b /ix
- MONOLOGUE_TOOL_INTENT_RX =
Narrated-intent monologue without a structured tool call. Distinct from INCOMPLETE_FINAL_RX (polite handoff to the human).
/ \b( wait[,\s]+let'?s\s+try| let'?s\s+try\s+(one|to|again|hping|nmap|ping|sudo|shell|running|checking)| i\s+(?:will|'ll)\s+(?:just\s+)?(?:try|run|check|probe|scan|use)\b| actually,?\s+i\s+will\b| one\s+more\s+thing\b| if\s+it\s+fails\b.{0,80}\bthen\s+we\s+can\b| verification\s+complete\b| report\s+that\s+(?:the\s+)?verification\s+failed\b ) /ix
- ACT_REQUEST_RX =
/ \b(write|create|implement|fix|patch|replace|refactor|overwrite| add (?:a |the )?|update|install|delete|remove|rename| regenerate|rebuild|document) \b /ix
- FORCED_WRAP_RX =
/ forced\s+to\s+a\s+final|were\s+not\s+written|not\s+written\s+to\s+disk| resume\s+from\s+the\s+table|remaining\s+block| were\s+not\s+applied|not\s+applied\s+in\s+this\s+turn| (?:do\s+that\s+)?next\s+time /ix
- LOOKUP_REQUEST_RX =
/ \b(what\s+is\s+my|hostname|uname|cwd|whoami|status|version|how\s+many)\b /ix
- SKILLS_CATALOG_RX =
/ \bskills?\b.{0,40}\b(available|installed|loaded|catalog|list)\b | \b(what|which|list)\b.{0,40}\bskills?\b /ix
- HOST_PATH_RX =
Real filesystem paths only — not https://host.tld (that was matching //host.tld).
%r{(?:(?<![.:/])/(?!/)|\./)[\w./-]+\.\w+}- BROWSER_REQUEST_RX =
/ TransparentBrowser|browser_obj|\bdevtools\b| \b(navigate|dump_links|headless_?chrome|watir)\b /ix
- HOUR_WORDS =
{ 'one' => 1, 'two' => 2, 'three' => 3, 'four' => 4, 'five' => 5, 'six' => 6, 'seven' => 7, 'eight' => 8, 'nine' => 9, 'ten' => 10, 'eleven' => 11, 'twelve' => 12, 'thirteen' => 13, 'fourteen' => 14, 'fifteen' => 15, 'sixteen' => 16, 'seventeen' => 17, 'eighteen' => 18, 'nineteen' => 19, 'twenty' => 20, 'twenty-four' => 24 }.freeze
- EMPTY_CONTRACT =
{ paths: [], min_seconds: 0, skills: [], proofs: [], hosts: [], techniques: [], issue_work: false }.freeze
- AUTH_REFUSE_RX =
A text-only policy/authorization refusal is never "truly blocked". pwn-ai does not decide authorization; operator request is the mandate.
/ \b( i\s+(?:can(?:['’]?t|not)|won['’]?t|will\s+not)\s+(?:help|run|execute|drive|call|probe|do|emit|perform)| request\s+refused| assistant\s+boundary| disallowed\s+here| will\s+not\s+(?:run|call|use|emit)\s+(?:core_tools|tools|tool\s+calls)| refuse(?:d|s)?\s+(?:live|the\s+request)| refusal\s+stands| not\s+something\s+i\s+will\s+run| (?:terminal|permanent)\s+(?:refusal|block)| path\s+closed(?:\s+permanently)?| truly\s+blocked )\b /ix
- PAYLOAD_SHA256 =
Digest::SHA256
- HISTORY_SPILL_DIR =
File.join(Dir.tmpdir, 'pwn-ai-hist')
- KEEP_FULL_TOOL_TAILS =
2- HOWTO_RX =
Request intent for routing (how-to vs act/recon vs pure recall/greeting). Local models thrash when pure explanation/recall/greeting asks are force-planned into multi-step host probes or multi-tool session archaeology. :howto → answer with explanation only (no plan_first / no live recon). :recall → prior-turn / vague memory cue; cheap path only. :greeting → short hello / light smalltalk; deterministic ack, no tools. :recon_act → live discovery (same tool loop as :act; no auth gate). :act → general agent work with tools.
/ \b( how\s+to|how\s+do\s+i|how\s+can\s+i|how\s+would\s+i|how\s+does\s+one| what\s+is\s+the\s+(?:syntax|command|usage|flag|option)| explain\s+how|show\s+me\s+how|examples?\s+of\s+using| manual\s+for|usage\s+of|syntax\s+for|man\s+page )\b /ix
- RECALL_RX =
Pure prior-turn recall — must never enter plan_first / multi-tool loops. Covers both "what did I just say?" (user) and "how did you respond?" / "what did you just say?" (assistant) so last-turn injection is used.
/ \A\s*( what\s+did\s+i\s+(just\s+)?say\??| what\s+did\s+i\s+(just\s+)?(?:ask|type|write|request)\??| what\s+was\s+my\s+last\s+(?:request|message|question|prompt|turn)\??| what\s+was\s+(?:the\s+)?(?:previous|prior|last)\s+(?:thing\s+i\s+said|request|message|turn)\??| remind\s+me\s+what\s+i\s+(?:just\s+)?(?:said|asked)\??| repeat\s+(?:my\s+)?(?:last|previous)\s+(?:request|message)\??| say\s+that\s+again\??| recollection\s+test\??| memory\s+recall\s+test\??| how\s+did\s+you\s+respond(?:\s+to\s+what\s+i\s+(?:just\s+)?(?:said|asked))?\??| how\s+did\s+you\s+(?:just\s+)?(?:answer|reply)(?:\s+to\s+(?:me|that|my\s+last))?\??| what\s+(?:was|is)\s+your\s+(?:last|previous|prior)\s+(?:answer|response|reply)\??| what\s+did\s+you\s+(?:just\s+)?(?:say|answer|reply|respond)\??| remind\s+me\s+what\s+you\s+(?:just\s+)?(?:said|answered|replied)\??| repeat\s+your\s+(?:last|previous)\s+(?:answer|response|reply)\?? )\s*\z /ix
- VAGUE_MEMORY_RX =
Broader "use your memory / prior context" cues. Still cheap: inject last turn + at most one memory_recall; never multi-step plans.
/ \b( what\s+did\s+i\s+(just\s+)?(?:say|ask|type|request)| what\s+was\s+my\s+last| how\s+did\s+you\s+respond| what\s+did\s+you\s+(?:just\s+)?(?:say|answer|reply|respond)| what\s+(?:was|is)\s+your\s+(?:last|previous|prior)\s+(?:answer|response|reply)| (?:without\s+looking\s+up).{0,40}(?:session|discussing|talking)| from\s+(?:(?:your|my|the)\s+)?(?:memory|context)| in\s+your\s+memory| (?:your|my)\s+memory\s+(?:of|about)| earlier\s+in\s+(?:this\s+)?(?:session|chat|conversation|turn)| previously\s+in\s+(?:this\s+)?(?:session|chat|conversation)| prior\s+turn| (?:do\s+you\s+)?remember\s+what\s+(?:i|you)| recall\s+(?:what|my|your|the\s+last)| last\s+thing\s+(?:i|you)\s+said )\b /ix
- LAST_SESSION_RX =
/\b(?:in|from|of)\s+(?:the\s+)?(?:last|previous|prior)\s+session\b|\blast\s+session\b/i- GREETING_RX =
Pure greeting / light smalltalk — never full :act tool loop. Anchored short forms only so "hi, please scan X" stays :act/:recon_act. Do NOT echo weather or invent social filler; answer_greeting is fixed.
/ \A\s*( (?:hi|hello|howdy|hey|yo|sup|hiya|greetings)(?:\s*[.!?]*)? (?:\s*,?\s*(?:there|all|folks|team|everyone|y'?all))? | good\s+(?:morning|afternoon|evening|day|night)(?:\s*[.!?]*)? | (?:hi|hello|howdy|hey)(?:\s*[.!?*,]*)?\s+ (?:it'?s|its|it\s+is)\s+ (?:cloudy|sunny|rainy|raining|foggy|windy|stormy|nice|cold|hot|warm| beautiful|gloomy|overcast|clear|chilly|humid|snow(?:ing|y)?) (?:\s+out(?:\s+there)?)?(?:\s*[.!?]*)? | (?:hi|hello|howdy|hey)(?:\s*[.!?*,]*)?\s+ (?:the\s+weather\s+is\s+\w+|what'?s\s+up|how\s+are\s+you| how'?s\s+it\s+going|how\s+goes\s+it) (?:\s*[.!?]*)? )\s*\z /ix
- LIVE_RECON_RX =
/ \b( (?:find|discover|enumerate|scan|sweep|probe|map)\s+ (?:live\s+)?(?:hosts?|ips?|targets?|subnet|network|range)| live\s+hosts?\s+(?:can\s+you\s+)?find| what\s+live\s+hosts| ping\s+sweep\s+(?:of\s+)?(?:this|the|my)\s+ |(?:run|do|perform)\s+(?:a\s+)?(?:ping\s+)?sweep |scan\s+(?:this|the|my)\s+(?:subnet|network|lan|range) )\b /ix
Class Method Summary collapse
-
.authors ⇒ Object
- Author(s)
0day Inc.
- .budget_status(opts = {}) ⇒ Object
-
.catalog_lookup?(opts = {}) ⇒ Boolean
True only when the ask needs a live host/file/browser effect.
- .debug_on?(opts = {}) ⇒ Boolean
- .evidence_satisfied?(opts = {}) ⇒ Boolean
- .help ⇒ Object
- .needs_host_work?(opts = {}) ⇒ Boolean
-
.ollama_wire_messages(opts = {}) ⇒ Object
- Supported Method Parameters
wire = PWN::AI::Agent::Loop.ollama_wire_messages( messages: 'required - in-memory OpenAI-ish messages (may have String args)' ).
-
.openai_wire_messages(opts = {}) ⇒ Object
- Supported Method Parameters
wire = PWN::AI::Agent::Loop.openai_wire_messages( messages: 'required - in-memory OpenAI-ish messages (may have Hash args / internal keys)' ).
- .request_intent(opts = {}) ⇒ Object
-
.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)' ).
- .world_knowledge?(opts = {}) ⇒ Boolean
Class Method Details
.authors ⇒ Object
- Author(s)
0day Inc. [email protected]
3684 3685 3686 |
# File 'lib/pwn/ai/agent/loop.rb', line 3684 public_class_method def self. "AUTHOR(S):\n 0day Inc. <[email protected]>\n" end |
.budget_status(opts = {}) ⇒ Object
3668 3669 3670 3671 3672 3673 3674 3675 3676 3677 3678 3679 3680 |
# File 'lib/pwn/ai/agent/loop.rb', line 3668 public_class_method def self.budget_status(opts = {}) t0 = opts[:t0] || Thread.current[:pwn_loop_t0] || Time.now elapsed = Time.now - t0 remain = (opts[:remaining_s] || Thread.current[:pwn_loop_budget_s] || 10_800).to_f - elapsed { elapsed_s: elapsed.round, remaining_tool_budget_s: [remain, 0].max.round, mutations_used: Thread.current[:pwn_loop_mutations].to_i, mutations_max: 10, context_tokens_used: Thread.current[:pwn_loop_tokens].to_i, est_max: 128_000 } end |
.catalog_lookup?(opts = {}) ⇒ Boolean
True only when the ask needs a live host/file/browser effect. World-knowledge questions ("what color is a cherry") do not.
677 678 679 680 681 682 683 684 685 686 687 |
# File 'lib/pwn/ai/agent/loop.rb', line 677 public_class_method def self.catalog_lookup?(opts = {}) request = opts[:request].to_s.strip return false if request.empty? return false if request.length > 120 return false if request.match?(ACT_REQUEST_RX) return false if request.match?(HOWTO_RX) request.match?(SKILLS_CATALOG_RX) rescue StandardError false end |
.debug_on?(opts = {}) ⇒ Boolean
248 249 250 251 252 253 254 255 256 257 |
# File 'lib/pwn/ai/agent/loop.rb', line 248 public_class_method def self.debug_on?(opts = {}) return true if opts[:debug] return true if defined?(PWN::Plugins::Log) && PWN::Plugins::Log.debug_enabled? pry_on = defined?(Pry) && Pry.respond_to?(:config) && Pry.config.respond_to?(:pwn_ai_debug) && Pry.config.pwn_ai_debug return true if pry_on false end |
.evidence_satisfied?(opts = {}) ⇒ Boolean
234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/pwn/ai/agent/loop.rb', line 234 public_class_method def self.evidence_satisfied?(opts = {}) = Array(opts[:messages] || opts[:trace]) text = opts[:text].to_s return false if TurnFinalizer.required_artifacts(request: opts[:request]).any? && completion_unmet(request: opts[:request], messages: ).any? if defined?(TurnFinalizer) && TurnFinalizer.respond_to?(:arbitrate) row = TurnFinalizer.arbitrate(request: opts[:request].to_s, messages: , paths: []) return true if row[:complete] && row[:unmet].empty? && row[:ledger].any? { |_p, v| v[:write] && v[:read] } end write_or_read_evidenced?(messages: ) && !text.strip.empty? rescue StandardError false end |
.help ⇒ Object
3729 3730 3731 3732 3733 3734 3735 3736 3737 3738 3739 3740 3741 3742 3743 3744 3745 3746 3747 3748 3749 3750 3751 3752 3753 3754 3755 3756 3757 3758 3759 3760 3761 3762 3763 3764 3765 3766 3767 3768 3769 3770 3771 3772 3773 3774 3775 3776 3777 3778 3779 3780 3781 3782 3783 3784 3785 3786 3787 3788 3789 3790 3791 3792 3793 3794 3795 3796 3797 3798 3799 3800 3801 3802 3803 3804 3805 3806 3807 3808 |
# File 'lib/pwn/ai/agent/loop.rb', line 3729 public_class_method def self.help puts "USAGE: # Run debug on and return its result #{self}.debug_on?( debug: 'optional - debug value consumed by #debug_on?' ) # True when write-then-readback evidence satisfies the original request. #{self}.evidence_satisfied?( messages: 'optional - Array of role/content hashes', trace: 'optional - alias for messages', text: 'optional - final answer text', request: 'optional - original request' ) # True only when the ask needs a live host/file/browser effect. World-knowledge #{self}.catalog_lookup?( request: 'required - request value consumed by #catalog_lookup?' ) # Run world knowledge and return its result #{self}.world_knowledge?( request: 'required - request value consumed by #world_knowledge?' ) # Run needs host work and return its result #{self}.needs_host_work?( request: 'required - request value consumed by #needs_host_work?' ) # Run ollama wire messages and return its result #{self}.ollama_wire_messages( messages: 'required - in-memory OpenAI-ish messages (may have String args)' ) # Run openai wire messages and return its result #{self}.openai_wire_messages( messages: 'required - in-memory OpenAI-ish messages (may have Hash args / internal keys)' ) # Run request intent and return its result #{self}.request_intent( request: 'optional - request value consumed by #request_intent' ) # Run run and return its result #{self}.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)', verification_contract: 'optional - host-owned Verification.run checks; execute at final boundary and attribute observed artifacts', steering: 'optional - request-owned Loop::Steering control; REPL owns its terminal reader and restarts with revised completion scope', trusted_context: 'optional - host-observed capability/prerequisite scope; never copied from model arguments', debug: 'optional - debug value consumed by #run', from: 'optional - sender account or address to bind as operator', account: 'optional - operator account id to bind', force_tools: 'optional - force tools value consumed by #run', nested: 'optional - true for Swarm/child Loop.run (skip RN footer)', engine: 'optional - provider name; defaults to PWN::Env ai.active', core_only: 'optional - restrict to CORE_TOOLS plus relevant MCP discovery when true', trace: 'optional - enable TracePoint debug for this run', debug_tee: 'optional - IO to tee debug logs', unattended: 'optional - true starts a durable mission and fails closed without an approved DAG', mission_id: 'optional - mission identifier consumed by #run', min_seconds: 'optional - named duration that keeps the mission open' ) # Remaining time/token/mutation budget for the current loop. #{self}.budget_status( t0: 'optional - session start Time (defaults to thread t0)', remaining_s: 'optional - override remaining tool budget seconds' ) # Print the AUTHOR(S) string for this module. #{self}.authors " constants.sort end |
.needs_host_work?(opts = {}) ⇒ Boolean
706 707 708 709 710 711 712 713 714 715 |
# File 'lib/pwn/ai/agent/loop.rb', line 706 public_class_method def self.needs_host_work?(opts = {}) request = opts[:request].to_s return false if request.strip.empty? return false if world_knowledge?(request: request) return false if catalog_lookup?(request: request) true rescue StandardError false end |
.ollama_wire_messages(opts = {}) ⇒ Object
- Supported Method Parameters
wire = PWN::AI::Agent::Loop.ollama_wire_messages( messages: 'required - in-memory OpenAI-ish messages (may have String args)' )
Returns a deep-copied array safe for Ollama / Open WebUI ollama/api/chat:
- parses JSON-string function.arguments into Hash/Array objects
- coerces nil assistant content to '' when tool_calls present (Open WebUI GenerateChatCompletionForm rejects content:null alone)
- drops _native_content / _text_tool_coerced / thinking private keys
- stringifies Hash/Array message content (tool results) to JSON text
1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 |
# File 'lib/pwn/ai/agent/loop.rb', line 1794 public_class_method def self.(opts = {}) = opts[:messages] Array().filter_map do |m| next unless m.is_a?(Hash) role = (m[:role] || m['role']).to_s out = { role: role } tcs = m[:tool_calls] || m['tool_calls'] wired_tcs = nil if tcs wired_tcs = Array(tcs).filter_map { |tc| ollama_wire_tool_call(tool_call: tc) } out[:tool_calls] = wired_tcs unless wired_tcs.empty? end if m.key?(:content) || m.key?('content') content = m.key?(:content) ? m[:content] : m['content'] out[:content] = case content when nil # Open WebUI: null content without tool_calls 400s; # with tool_calls prefer "" over null. wired_tcs && !wired_tcs.empty? ? '' : nil when String then content when Hash, Array then JSON.generate(content) else content.to_s end elsif wired_tcs && !wired_tcs.empty? out[:content] = '' end name = m[:name] || m['name'] out[:name] = name.to_s if name && !name.to_s.empty? tcid = m[:tool_call_id] || m['tool_call_id'] out[:tool_call_id] = tcid.to_s if tcid && !tcid.to_s.empty? out end end |
.openai_wire_messages(opts = {}) ⇒ Object
- Supported Method Parameters
wire = PWN::AI::Agent::Loop.openai_wire_messages( messages: 'required - in-memory OpenAI-ish messages (may have Hash args / internal keys)' )
Returns a deep-copied array safe for OpenAI / xAI chat.completions:
- drops _native_content / _text_tool_coerced / thinking private keys
- stringifies function.arguments maps
- coerces Hash/non-string content to JSON/string (nil kept for assistant tool turns)
1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 |
# File 'lib/pwn/ai/agent/loop.rb', line 1878 public_class_method def self.(opts = {}) = opts[:messages] Array().filter_map do |m| next unless m.is_a?(Hash) role = (m[:role] || m['role']).to_s out = { role: role } if m.key?(:content) || m.key?('content') content = m.key?(:content) ? m[:content] : m['content'] out[:content] = case content when nil then nil when String then content when Hash, Array then JSON.generate(content) else content.to_s end end name = m[:name] || m['name'] out[:name] = name.to_s if name && !name.to_s.empty? tcid = m[:tool_call_id] || m['tool_call_id'] out[:tool_call_id] = tcid.to_s if tcid && !tcid.to_s.empty? tcs = m[:tool_calls] || m['tool_calls'] if tcs wired = Array(tcs).filter_map { |tc| openai_wire_tool_call(tool_call: tc) } out[:tool_calls] = wired unless wired.empty? end out end end |
.request_intent(opts = {}) ⇒ Object
2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 |
# File 'lib/pwn/ai/agent/loop.rb', line 2455 public_class_method def self.request_intent(opts = {}) req = opts[:request].to_s return :empty if req.strip.empty? # Pure greeting / weather smalltalk before how-to/recon/act. # Deterministic short-circuit — never freeform model weather echo. return :greeting if req.match?(GREETING_RX) # Pure prior-turn recall before how-to/recon (short, decisive). return :recall if req.match?(RECALL_RX) # Vague memory cues that are still "about the prior turn" and not # general work ("remember what we decided about nmap and implement it" # stays :act because it pairs memory with a doing verb outside the cue). if req.match?(VAGUE_MEMORY_RX) && !req.match?(HOWTO_RX) && !req.match?(LIVE_RECON_RX) doing = req.match?( /\b(implement|fix|patch|refactor|run|execute|scan|write|edit| change|deploy|install|build|compile|commit|push)\b/ix ) return :recall unless doing end if req.match?(LAST_SESSION_RX) && !req.match?(HOWTO_RX) && !req.match?(LIVE_RECON_RX) doing = req.match?( /\b(implement|fix|patch|refactor|run|execute|scan|write|edit| change|deploy|install|build|compile|commit|push)\b/ix ) return :recall unless doing end # Live-action recon takes precedence over bare "how to" when both appear # only if the user clearly asks the agent to do the sweep here. live = req.match?(LIVE_RECON_RX) && req.match?( /\b(can\s+you|could\s+you|please|go\s+ahead|now|on\s+this\s+host| this\s+subnet|this\s+network|find\s+(?:for\s+me|me)|discover)\b/ix ) return :recon_act if live || (req.match?(LIVE_RECON_RX) && !req.match?(HOWTO_RX)) return :howto if req.match?(HOWTO_RX) # Interrogative documentation without "how to" if req.match?(/\b(what\s+(?:flags?|options?|switches?)|usage|syntax)\b/i) && !req.match?(/\b(run|execute|scan|find|discover)\b/i) return :howto end :act rescue StandardError :act 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)' )
3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 |
# File 'lib/pwn/ai/agent/loop.rb', line 3038 public_class_method def self.run(opts = {}) = opts.merge(request: opts[:request]) original_system = opts[:system_role_content] begin run_turn() rescue Steering::Restart => e history = e..dup history.pop if history.last&.dig(:role).to_s == 'assistant' && Array(history.last[:tool_calls]).empty? repair_steering_history!(messages: history) e.instructions.each do |instruction| history << { role: 'user', content: instruction } append_session(session_id: [:session_id], role: 'user', content: instruction) end = .merge(request: e.instructions.last, steering_history: history, system_role_content: original_system) update_steered_open_goal!() .delete(:verification_contract) retry end end |
.world_knowledge?(opts = {}) ⇒ Boolean
689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 |
# File 'lib/pwn/ai/agent/loop.rb', line 689 public_class_method def self.world_knowledge?(opts = {}) request = opts[:request].to_s.strip return false if request.empty? return false if request.length > 120 return false if catalog_lookup?(request: request) return false if request.match?(ACT_REQUEST_RX) return false if request.match?(LOOKUP_REQUEST_RX) return false if request.match?(HOST_PATH_RX) return false if request.match?(BROWSER_REQUEST_RX) return false if request.match?(HOWTO_RX) return false if request.match?(%r{\b(this\s+(?:host|machine|box|system|subnet|file|repo)|/opt/|implement|scan|hosts?)\b}i) request.match?(/\A(?:what|why|who|when|where|which|how)\b/i) rescue StandardError false end |