pwn 0.5.697 → 0.5.698
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 +33 -1
- data/lib/pwn/ai/agent/task_summarizer.rb +2 -2
- 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 +7 -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: c333795b07ea88fb7ea338e6b01faf768c62bd07757328fc1698972d43a6c5e8
|
|
4
|
+
data.tar.gz: 0cfbe454002f7ffc79571162a96367aaf3d69017d997cb024c4a475ac3bbab7b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a699a8308cb00f04bdf85869647ec381009daf04fe4d8c972c62d5ed57b819b4b719672fe4ad64966c316520a2496bdeb4914c002f526e886b877f7b5d11928
|
|
7
|
+
data.tar.gz: 5b2d1fa1f979bc4852304ed09b7122817a71689114f4c4f6a1a46798b01e9d3bb186b8af28aa8d08288c470b01d323735fb252ccc9c45426835d8b717ca8e382
|
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.
|
|
@@ -2473,6 +2484,7 @@ module PWN
|
|
|
2473
2484
|
|
|
2474
2485
|
turn_fails = Hash.new(0)
|
|
2475
2486
|
escalated = false
|
|
2487
|
+
engine_blips = 0
|
|
2476
2488
|
maybe_park_budget_scars!
|
|
2477
2489
|
maybe_extinguish_parked!
|
|
2478
2490
|
|
|
@@ -2486,7 +2498,27 @@ module PWN
|
|
|
2486
2498
|
inject_task_focus!(messages: messages, state: ts_state, request: request) unless skip_compass
|
|
2487
2499
|
|
|
2488
2500
|
t0 = Time.now
|
|
2489
|
-
|
|
2501
|
+
begin
|
|
2502
|
+
msg = call_engine(messages: messages, tools: tools, ts_state: ts_state)
|
|
2503
|
+
rescue StandardError => e
|
|
2504
|
+
if engine_transient?(error: e)
|
|
2505
|
+
engine_blips += 1
|
|
2506
|
+
debug_progress(msg: "engine hop failed #{e.class}: #{e.message.to_s[0, 240]} blip=#{engine_blips}")
|
|
2507
|
+
if engine_blips >= 5
|
|
2508
|
+
txt = "[pwn-ai] engine hop failed after #{engine_blips} tries: #{e.message.to_s[0, 240]}"
|
|
2509
|
+
debug_final_text!(text: txt)
|
|
2510
|
+
final_chars = txt.length
|
|
2511
|
+
return txt
|
|
2512
|
+
end
|
|
2513
|
+
messages << {
|
|
2514
|
+
role: 'user',
|
|
2515
|
+
content: '[pwn-ai] engine hop failed (transient). Keep calling CORE_TOOLS. ' \
|
|
2516
|
+
"Do not stop. (#{e.message.to_s[0, 180]})"
|
|
2517
|
+
}
|
|
2518
|
+
next
|
|
2519
|
+
end
|
|
2520
|
+
raise
|
|
2521
|
+
end
|
|
2490
2522
|
engine_s += (Time.now - t0)
|
|
2491
2523
|
PWN::Plugins::TTYSpinner.halt_all! if defined?(PWN::Plugins::TTYSpinner)
|
|
2492
2524
|
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
|
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,16 @@ 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
|
+
|
|
430
437
|
it 'records a timeout increment mistake instead of treating success:true as ok' do
|
|
431
438
|
tmp = Dir.mktmpdir
|
|
432
439
|
stub_const('PWN::AI::Agent::Mistakes::MISTAKES_FILE', File.join(tmp, 'mistakes.json'))
|
|
@@ -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`: "}]}
|