pwn 0.5.697 → 0.5.699
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/etc/default_skills/pwn/ai/http_retry/SKILL.md +1 -0
- data/lib/pwn/ai/agent/loop.rb +41 -5
- data/lib/pwn/ai/agent/task_summarizer.rb +13 -2
- data/lib/pwn/ai/agent/tool_guard.rb +1 -0
- data/lib/pwn/ai/agent/tools/shell.rb +1 -1
- data/lib/pwn/ai/http_retry.rb +9 -0
- data/lib/pwn/ai/ollama.rb +15 -0
- data/lib/pwn/plugins/repl.rb +3 -2
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/loop_spec.rb +34 -0
- data/spec/lib/pwn/ai/agent/signal_hygiene_spec.rb +8 -0
- data/spec/lib/pwn/ai/agent/task_summarizer_spec.rb +10 -0
- data/spec/lib/pwn/ai/http_retry_spec.rb +8 -0
- data/spec/lib/pwn/plugins/repl_pwn_vault_spec.rb +10 -0
- data/third_party/pwn_rdoc.jsonl +2 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5d7edeea1940ad56143190285565ba9bd76f4d78a47294238eafaad1d002686f
|
|
4
|
+
data.tar.gz: c3a594906ca8d239b8f5dfc3fe3343ec718bf1a71e3c85334b3b6db7e852b5fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fee1edba85d565609b592498feff5eaf38bc01a4771af2a2e22f3719d1eac7faaf54c8f8772837358cf5cbf18db59736aee90fe4a7114c1e6353be423d42e2e8
|
|
7
|
+
data.tar.gz: 4ad32cb15b7bd78d0fa150cf63379ecf61184b84a902426a3581a558b48b17b0a7bc23048a2d2576155b538403e9472a1ebe542a8154eece95a36e4106e352f9
|
data/lib/pwn/ai/agent/loop.rb
CHANGED
|
@@ -851,6 +851,17 @@ module PWN
|
|
|
851
851
|
opts[:session_id].to_s
|
|
852
852
|
end
|
|
853
853
|
|
|
854
|
+
private_class_method def self.engine_transient?(opts = {})
|
|
855
|
+
err = opts[:error]
|
|
856
|
+
return false unless err
|
|
857
|
+
|
|
858
|
+
return PWN::AI::HttpRetry.retryable?(error: err) if defined?(PWN::AI::HttpRetry) && PWN::AI::HttpRetry.respond_to?(:retryable?)
|
|
859
|
+
|
|
860
|
+
err.message.to_s.match?(/HTTP 50[234]|Gateway Time-out|stream absolute timeout/i)
|
|
861
|
+
rescue StandardError
|
|
862
|
+
false
|
|
863
|
+
end
|
|
864
|
+
|
|
854
865
|
# E1 — did the environment change under this tool? If Metrics CUSUM
|
|
855
866
|
# tripped for it in the last hour AND Extrospection.drift shows a
|
|
856
867
|
# toolchain/net/repo change, blame the WORLD not the AGENT.
|
|
@@ -1416,8 +1427,10 @@ module PWN
|
|
|
1416
1427
|
messages = opts[:messages]
|
|
1417
1428
|
return messages unless messages.is_a?(Array) && messages.length > 12
|
|
1418
1429
|
|
|
1419
|
-
keep_pairs =
|
|
1420
|
-
|
|
1430
|
+
keep_pairs = opts[:keep_pairs]
|
|
1431
|
+
keep_pairs = (agent_flag(key: :history_keep_tool_pairs, default: 6) || 6).to_i if keep_pairs.nil?
|
|
1432
|
+
max_chars = opts[:max_chars]
|
|
1433
|
+
max_chars = (agent_flag(key: :history_tool_max_chars, default: 2_000) || 2_000).to_i if max_chars.nil?
|
|
1421
1434
|
|
|
1422
1435
|
head = []
|
|
1423
1436
|
rest = messages.dup
|
|
@@ -2473,20 +2486,43 @@ module PWN
|
|
|
2473
2486
|
|
|
2474
2487
|
turn_fails = Hash.new(0)
|
|
2475
2488
|
escalated = false
|
|
2489
|
+
engine_blips = 0
|
|
2476
2490
|
maybe_park_budget_scars!
|
|
2477
2491
|
maybe_extinguish_parked!
|
|
2478
2492
|
|
|
2479
2493
|
i = 0
|
|
2480
2494
|
loop do
|
|
2481
2495
|
i += 1
|
|
2482
|
-
# 3.1 — compact
|
|
2483
|
-
|
|
2496
|
+
# 3.1 — compact fat tool dumps so remote ReadTimeout hops do not
|
|
2497
|
+
# retry the same 70-message payload (R1 201215 Anthropic 180s×5).
|
|
2498
|
+
compact_history!(messages: messages)
|
|
2484
2499
|
# English-task-as-primary: when plan_idx advanced, tell the model
|
|
2485
2500
|
# which plain-English task is active before the next tool batch.
|
|
2486
2501
|
inject_task_focus!(messages: messages, state: ts_state, request: request) unless skip_compass
|
|
2487
2502
|
|
|
2488
2503
|
t0 = Time.now
|
|
2489
|
-
|
|
2504
|
+
begin
|
|
2505
|
+
msg = call_engine(messages: messages, tools: tools, ts_state: ts_state)
|
|
2506
|
+
rescue StandardError => e
|
|
2507
|
+
if engine_transient?(error: e)
|
|
2508
|
+
engine_blips += 1
|
|
2509
|
+
debug_progress(msg: "engine hop failed #{e.class}: #{e.message.to_s[0, 240]} blip=#{engine_blips}")
|
|
2510
|
+
if engine_blips >= 5
|
|
2511
|
+
txt = "[pwn-ai] engine hop failed after #{engine_blips} tries: #{e.message.to_s[0, 240]}"
|
|
2512
|
+
debug_final_text!(text: txt)
|
|
2513
|
+
final_chars = txt.length
|
|
2514
|
+
return txt
|
|
2515
|
+
end
|
|
2516
|
+
compact_history!(messages: messages, keep_pairs: 3, max_chars: 800)
|
|
2517
|
+
messages << {
|
|
2518
|
+
role: 'user',
|
|
2519
|
+
content: '[pwn-ai] engine hop failed (transient). Keep calling CORE_TOOLS. ' \
|
|
2520
|
+
"Do not stop. (#{e.message.to_s[0, 180]})"
|
|
2521
|
+
}
|
|
2522
|
+
next
|
|
2523
|
+
end
|
|
2524
|
+
raise
|
|
2525
|
+
end
|
|
2490
2526
|
engine_s += (Time.now - t0)
|
|
2491
2527
|
PWN::Plugins::TTYSpinner.halt_all! if defined?(PWN::Plugins::TTYSpinner)
|
|
2492
2528
|
if msg.nil?
|
|
@@ -573,8 +573,8 @@ module PWN
|
|
|
573
573
|
# model cannot stall the user's real answer, and stay quiet on timeout.
|
|
574
574
|
private_class_method def self.sidecar_timeout
|
|
575
575
|
n = (PWN::Env.dig(:ai, :agent, :sidecar_llm_timeout) if defined?(PWN::Env)).to_i
|
|
576
|
-
n =
|
|
577
|
-
n =
|
|
576
|
+
n = 180 if n < 30
|
|
577
|
+
n = 180 if n > 180
|
|
578
578
|
n
|
|
579
579
|
rescue StandardError
|
|
580
580
|
20
|
|
@@ -661,6 +661,17 @@ module PWN
|
|
|
661
661
|
]
|
|
662
662
|
end
|
|
663
663
|
|
|
664
|
+
artifact = goal_text[%r{(?:/(?:tmp|var|home|opt|usr)/\S+\.(?:pdf|html|md|json|txt|csv)|~/\S+\.(?:pdf|html|md|json|txt|csv))}i]
|
|
665
|
+
if artifact && goal_lc.match?(/\b(analy[sz]e|test|scan|report|generat|store|write|export)\b/)
|
|
666
|
+
tasks << 'Carry out the requested analysis using the named skills and live evidence'
|
|
667
|
+
tasks << "Write the requested report to #{artifact}"
|
|
668
|
+
if goal_lc.match?(/\b(json|ya?ml|table|csv|tsv)\b/)
|
|
669
|
+
fmt = goal_lc[/\b(json|ya?ml|table|csv|tsv)\b/]
|
|
670
|
+
tasks << "Present the results in #{fmt} format"
|
|
671
|
+
end
|
|
672
|
+
return tasks
|
|
673
|
+
end
|
|
674
|
+
|
|
664
675
|
tasks << "Understand the request: #{truncate_goal(goal: goal_text)}"
|
|
665
676
|
tasks << "Carry out the core work for: #{truncate_goal(goal: goal_text)}"
|
|
666
677
|
|
|
@@ -65,7 +65,7 @@ PWN::AI::Agent::Registry.register(
|
|
|
65
65
|
|
|
66
66
|
if PWN::AI::Agent::ToolGuard.bashism?(text: cmd) && !PWN::AI::Agent::ToolGuard.shell_bash?
|
|
67
67
|
return PWN::AI::Agent::ToolGuard.invalid_payload(
|
|
68
|
-
hint: 'Command uses bash-only syntax (PIPESTATUS, [[ ]], process substitution, ' \
|
|
68
|
+
hint: 'Command uses bash-only syntax (PIPESTATUS, $RANDOM, [[ ]], process substitution, ' \
|
|
69
69
|
'source, &>). This handler runs /bin/sh (dash) unless ' \
|
|
70
70
|
'PWN::Env[:ai][:agent][:shell_bash]=true. Rewrite as POSIX or opt in to bash.'
|
|
71
71
|
)
|
data/lib/pwn/ai/http_retry.rb
CHANGED
|
@@ -27,6 +27,15 @@ module PWN
|
|
|
27
27
|
DEFAULT_MAX_ATTEMPTS
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
public_class_method def self.retryable?(opts = {})
|
|
31
|
+
err = opts[:error]
|
|
32
|
+
msg = err.respond_to?(:message) ? err.message.to_s : err.to_s
|
|
33
|
+
msg = opts[:message].to_s if msg.empty?
|
|
34
|
+
msg.match?(/HTTP 50[234]|Gateway Time-out|stream absolute timeout|ReadTimeout|Net::ReadTimeout|Timed out reading/i)
|
|
35
|
+
rescue StandardError
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
|
|
30
39
|
public_class_method def self.retry_after_s(opts = {})
|
|
31
40
|
retry_count = [opts[:retry_count].to_i, 1].max
|
|
32
41
|
retry_after = 0
|
data/lib/pwn/ai/ollama.rb
CHANGED
|
@@ -198,6 +198,21 @@ module PWN
|
|
|
198
198
|
retry if retry_count < max_attempts
|
|
199
199
|
|
|
200
200
|
nil
|
|
201
|
+
rescue StandardError => e
|
|
202
|
+
if PWN::AI::HttpRetry.retryable?(error: e) && retry_count + 1 < max_attempts
|
|
203
|
+
retry_count += 1
|
|
204
|
+
unless opts[:quiet]
|
|
205
|
+
PWN::AI::HttpRetry.report_event(
|
|
206
|
+
label: 'ollama', which_self: self, quiet: opts[:quiet],
|
|
207
|
+
http_method: http_method, rest_call: rest_call,
|
|
208
|
+
extra: "retryable attempt=#{retry_count}/#{max_attempts}",
|
|
209
|
+
error: e
|
|
210
|
+
)
|
|
211
|
+
end
|
|
212
|
+
sleep(PWN::AI::HttpRetry.retry_after_s(retry_count: retry_count) + rand(0.3..2.0))
|
|
213
|
+
retry
|
|
214
|
+
end
|
|
215
|
+
raise e
|
|
201
216
|
end
|
|
202
217
|
rescue RestClient::ExceptionWithResponse => e
|
|
203
218
|
body = begin
|
data/lib/pwn/plugins/repl.rb
CHANGED
|
@@ -1284,8 +1284,9 @@ module PWN
|
|
|
1284
1284
|
raise
|
|
1285
1285
|
rescue StandardError => e
|
|
1286
1286
|
PWN::Plugins::Log.note_exception!(error: e, where: 'native agent loop', which_self: PWN::Plugins::REPL) if defined?(PWN::Plugins::Log) && PWN::Plugins::Log.respond_to?(:note_exception!)
|
|
1287
|
-
warn "[pwn-ai] native agent loop failed (#{e.class}: #{e.message
|
|
1288
|
-
|
|
1287
|
+
warn "[pwn-ai] native agent loop failed (#{e.class}: #{e.message.to_s.split("\n").first})"
|
|
1288
|
+
request.replace('nil')
|
|
1289
|
+
next
|
|
1289
1290
|
ensure
|
|
1290
1291
|
PWN::Plugins::REPL.ready_tty!
|
|
1291
1292
|
end
|
data/lib/pwn/version.rb
CHANGED
|
@@ -424,9 +424,43 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
|
|
|
424
424
|
expect(src).to match(/note_exception!/)
|
|
425
425
|
expect(src).to match(/rescue StandardError/)
|
|
426
426
|
expect(src).to include("debug_progress(msg: 'engine returned no message')")
|
|
427
|
+
expect(src).to match(/engine_transient|retryable\?/)
|
|
427
428
|
expect(src).not_to match(/dispatch #\{name\} ok_len=/)
|
|
428
429
|
end
|
|
429
430
|
|
|
431
|
+
it 'keeps Loop.run going after a transient engine 504' do
|
|
432
|
+
src = File.read(described_class.method(:run).source_location.first)
|
|
433
|
+
expect(src).to match(/engine_transient\?|HttpRetry\.retryable\?/)
|
|
434
|
+
expect(src).to match(/engine hop failed|engine_blip/)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
it 'compacts tool history for every engine, not only local' do
|
|
438
|
+
src = File.read(described_class.method(:run).source_location.first)
|
|
439
|
+
expect(src).to match(/compact_history!\(messages: messages\)/)
|
|
440
|
+
expect(src).not_to match(/compact_history!\(messages: messages\) if local/)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
it 'shrinks tool bodies when history is fat' do
|
|
444
|
+
msgs = [
|
|
445
|
+
{ role: 'system', content: 'sys' },
|
|
446
|
+
{ role: 'user', content: 'goal' }
|
|
447
|
+
]
|
|
448
|
+
8.times do |n|
|
|
449
|
+
msgs << { role: 'assistant', content: '', tool_calls: [{ id: "c#{n}" }] }
|
|
450
|
+
msgs << { role: 'tool', tool_call_id: "c#{n}", name: 'shell', content: ('x' * 4_000) }
|
|
451
|
+
end
|
|
452
|
+
out = described_class.send(:compact_history!, messages: msgs)
|
|
453
|
+
tool_bodies = out.select { |m| m[:role].to_s == 'tool' }
|
|
454
|
+
expect(tool_bodies.length).to be <= 6
|
|
455
|
+
expect(tool_bodies.map { |m| m[:content].to_s.length }.max).to be <= 2_100
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
it 'compacts again after a transient engine hop before retrying' do
|
|
459
|
+
src = File.read(described_class.method(:run).source_location.first)
|
|
460
|
+
hop = src[/if engine_transient\?.*?raise/m]
|
|
461
|
+
expect(hop).to match(/compact_history!/)
|
|
462
|
+
end
|
|
463
|
+
|
|
430
464
|
it 'records a timeout increment mistake instead of treating success:true as ok' do
|
|
431
465
|
tmp = Dir.mktmpdir
|
|
432
466
|
stub_const('PWN::AI::Agent::Mistakes::MISTAKES_FILE', File.join(tmp, 'mistakes.json'))
|
|
@@ -36,6 +36,14 @@ describe 'P0 signal hygiene (handler + inbox + policy-cold + calibrate)' do
|
|
|
36
36
|
expect(r[:hint].to_s).to match(/bash-only/i)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
it 'rejects $RANDOM under /bin/sh so the model rewrites instead of getting USER=' do
|
|
40
|
+
expect(PWN::AI::Agent::ToolGuard.bashism?(text: 'U="p4tester$RANDOM"; echo $U')).to eq(true)
|
|
41
|
+
handler = PWN::AI::Agent::Registry.lookup(name: 'shell').handler
|
|
42
|
+
r = handler.call(command: 'U="p4tester$RANDOM"; echo $U')
|
|
43
|
+
expect(r[:error]).to eq('invalid_payload')
|
|
44
|
+
expect(r[:hint].to_s).to match(/RANDOM|bash-only|POSIX/i)
|
|
45
|
+
end
|
|
46
|
+
|
|
39
47
|
it 'does not refuse pwn_eval host-discovery as unauthorized' do
|
|
40
48
|
src = File.read('/opt/pwn/lib/pwn/ai/agent/tools/ruby_eval.rb')
|
|
41
49
|
expect(src).not_to match(/recon_blocked/)
|
|
@@ -425,6 +425,16 @@ describe PWN::AI::Agent::TaskSummarizer do
|
|
|
425
425
|
)
|
|
426
426
|
end
|
|
427
427
|
|
|
428
|
+
it 'splits analyze-plus-artifact goals instead of pasting the full request twice' do
|
|
429
|
+
goal = 'Exhaustively analyze the running application for vulnerabilities. ' \
|
|
430
|
+
'Once complete, generate a complete PDF report and store in /tmp/container_pentest-p4.pdf'
|
|
431
|
+
tasks = described_class.fallback_decompose(goal: goal)
|
|
432
|
+
expect(tasks.length).to be >= 2
|
|
433
|
+
expect(tasks.join("\n")).to include('/tmp/container_pentest-p4.pdf')
|
|
434
|
+
expect(tasks.grep(/Understand the request:/).length).to eq(0)
|
|
435
|
+
expect(tasks.grep(/Carry out the core work for:/).length).to eq(0)
|
|
436
|
+
end
|
|
437
|
+
|
|
428
438
|
it 'accepts injected :tasks / :llm_tasks without network I/O' do
|
|
429
439
|
custom = ['step alpha', 'step beta', 'verify completion']
|
|
430
440
|
tasks = described_class.plan(request: 'anything at all', tasks: custom)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
4
|
require 'stringio'
|
|
5
|
+
require 'rest-client'
|
|
5
6
|
|
|
6
7
|
describe PWN::AI::HttpRetry do
|
|
7
8
|
it 'defaults timeout to 180 and max_attempts to 5' do
|
|
@@ -14,6 +15,12 @@ describe PWN::AI::HttpRetry do
|
|
|
14
15
|
expect(described_class.max_attempts(max_attempts: 3)).to eq(3)
|
|
15
16
|
end
|
|
16
17
|
|
|
18
|
+
it 'treats gateway 504 and stream absolute timeouts as retryable' do
|
|
19
|
+
expect(described_class.retryable?(error: 'ERROR: Ollama HTTP 504 for api/chat: Gateway Time-out')).to eq(true)
|
|
20
|
+
expect(described_class.retryable?(error: 'ERROR: Ollama stream absolute timeout after 20s for api/chat')).to eq(true)
|
|
21
|
+
expect(described_class.retryable?(error: '400 Bad Request')).to eq(false)
|
|
22
|
+
end
|
|
23
|
+
|
|
17
24
|
it 'tees emit lines into the open debug request log' do
|
|
18
25
|
path = PWN::Plugins::Log.start_debug(
|
|
19
26
|
tee: StringIO.new,
|
|
@@ -50,6 +57,7 @@ describe PWN::AI::HttpRetry do
|
|
|
50
57
|
expect(src).not_to match(/timeout \|\|= 900/)
|
|
51
58
|
expect(src).to match(/Exceptions::Timeout/)
|
|
52
59
|
expect(src).to match(/HttpRetry\.report_event/)
|
|
60
|
+
expect(src).to match(/retryable\?|HTTP 50/) if name == 'PWN::AI::Ollama'
|
|
53
61
|
end
|
|
54
62
|
end
|
|
55
63
|
end
|
|
@@ -174,4 +174,14 @@ describe 'pwn-ai debug final answer placement' do
|
|
|
174
174
|
expect(hook).to include('→ pwn-ai →')
|
|
175
175
|
expect(hook).to include('→ result')
|
|
176
176
|
end
|
|
177
|
+
|
|
178
|
+
it 'does not fall through to regex-ReAct after a native Loop.run error' do
|
|
179
|
+
native_start = hook.index('final = PWN::AI::Agent::Loop.run')
|
|
180
|
+
rescue_at = hook.index('rescue StandardError', native_start)
|
|
181
|
+
expect(rescue_at).not_to be_nil
|
|
182
|
+
chunk = hook[rescue_at, 900]
|
|
183
|
+
expect(chunk).to match(/request\.replace\('nil'\)/)
|
|
184
|
+
expect(chunk).to match(/\bnext\b/)
|
|
185
|
+
expect(chunk).not_to include('legacy regex-ReAct')
|
|
186
|
+
end
|
|
177
187
|
end
|
data/third_party/pwn_rdoc.jsonl
CHANGED
|
@@ -339,6 +339,7 @@
|
|
|
339
339
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.degrade_text_only Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.degrade_text_only`: "}]}
|
|
340
340
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.dispatch_fail_n Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.dispatch_fail_n`: "}]}
|
|
341
341
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.emit_task_summary Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.emit_task_summary`: "}]}
|
|
342
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.engine_transient? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.engine_transient?`: "}]}
|
|
342
343
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.escalate Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.escalate`: "}]}
|
|
343
344
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.evidence_enough_to_finalize? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.evidence_enough_to_finalize?`: "}]}
|
|
344
345
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Loop.expose_current_session Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Loop.expose_current_session`: "}]}
|
|
@@ -793,6 +794,7 @@
|
|
|
793
794
|
{"messages":[{"role":"user","content":"PWN::AI::HttpRetry.max_attempts Usage"},{"role":"assistant","content":"`PWN::AI::HttpRetry.max_attempts`: "}]}
|
|
794
795
|
{"messages":[{"role":"user","content":"PWN::AI::HttpRetry.report_event Usage"},{"role":"assistant","content":"`PWN::AI::HttpRetry.report_event`: "}]}
|
|
795
796
|
{"messages":[{"role":"user","content":"PWN::AI::HttpRetry.retry_after_s Usage"},{"role":"assistant","content":"`PWN::AI::HttpRetry.retry_after_s`: "}]}
|
|
797
|
+
{"messages":[{"role":"user","content":"PWN::AI::HttpRetry.retryable? Usage"},{"role":"assistant","content":"`PWN::AI::HttpRetry.retryable?`: "}]}
|
|
796
798
|
{"messages":[{"role":"user","content":"PWN::AI::HttpRetry.timeout_s Usage"},{"role":"assistant","content":"`PWN::AI::HttpRetry.timeout_s`: "}]}
|
|
797
799
|
{"messages":[{"role":"user","content":"PWN::AI::Ollama.assemble_native_chat_stream Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.assemble_native_chat_stream`: "}]}
|
|
798
800
|
{"messages":[{"role":"user","content":"PWN::AI::Ollama.assemble_ollama_stream Usage"},{"role":"assistant","content":"`PWN::AI::Ollama.assemble_ollama_stream`: "}]}
|