pwn 0.5.647 → 0.5.650
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/documentation/Agent-Tool-Registry.md +10 -5
- data/documentation/Home.md +1 -1
- data/documentation/How-PWN-Works.md +1 -1
- data/documentation/Reinforcement-Learning.md +36 -1
- data/documentation/What-is-PWN.md +1 -1
- data/documentation/pwn-ai-Agent.md +1 -1
- data/lib/pwn/ai/agent/curriculum.rb +125 -5
- data/lib/pwn/ai/agent/extrospection.rb +18 -4
- data/lib/pwn/ai/agent/learning.rb +171 -63
- data/lib/pwn/ai/agent/loop.rb +72 -9
- data/lib/pwn/ai/agent/metrics.rb +69 -10
- data/lib/pwn/ai/agent/registry.rb +21 -2
- data/lib/pwn/ai/agent/reward.rb +171 -5
- data/lib/pwn/ai/agent/tools/curriculum.rb +23 -0
- data/lib/pwn/ai/agent/tools/reward.rb +20 -0
- data/lib/pwn/ai/agent/tools/ruby_eval.rb +29 -6
- data/lib/pwn/version.rb +1 -1
- data/spec/integration/reinforced_feedback_loop_spec.rb +299 -0
- data/spec/lib/pwn/ai/agent/tools/ruby_eval_spec.rb +39 -1
- data/third_party/pwn_rdoc.jsonl +9 -2
- metadata +1 -1
|
@@ -44,6 +44,7 @@ RSpec.describe 'PWN::AI::Agent reinforced feedback loop', :aggregate_failures do
|
|
|
44
44
|
stub_const('PWN::AI::Agent::Reward::DPO_DIR', File.join(@tmp, 'finetune'))
|
|
45
45
|
stub_const('PWN::AI::Agent::Curriculum::CURRICULUM_DIR', File.join(@tmp, 'curriculum'))
|
|
46
46
|
stub_const('PWN::AI::Agent::Curriculum::MODELS_FILE', File.join(@tmp, 'curriculum', 'models.json'))
|
|
47
|
+
stub_const('PWN::AI::Agent::Curriculum::KPI_FILE', File.join(@tmp, 'curriculum_kpi.jsonl'))
|
|
47
48
|
stub_const('PWN::Sessions::SESSIONS_DIR', File.join(@tmp, 'sessions'))
|
|
48
49
|
stub_const('PWN::Memory::MEMORY_FILE', File.join(@tmp, 'memory.json'))
|
|
49
50
|
stub_const('PWN::MemoryIndex::INDEX_FILE', File.join(@tmp, 'memory.idx')) if defined?(PWN::MemoryIndex)
|
|
@@ -1201,5 +1202,303 @@ RSpec.describe 'PWN::AI::Agent reinforced feedback loop', :aggregate_failures do
|
|
|
1201
1202
|
expect(r[:verdict]).to eq :pass
|
|
1202
1203
|
end
|
|
1203
1204
|
end
|
|
1205
|
+
|
|
1206
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
1207
|
+
# Design-priority STATUS (post P14–P25) — P0 / P1 / P2
|
|
1208
|
+
# documentation/Reinforcement-Learning.md § Design-priority STATUS
|
|
1209
|
+
# ═══════════════════════════════════════════════════════════════════════
|
|
1210
|
+
|
|
1211
|
+
describe 'P0 · W1 generator diversity (TARGET_SOURCE_MIX + generator_mix)' do
|
|
1212
|
+
it 'exposes TARGET_SOURCE_MIX soft targets that sum to 1.0' do
|
|
1213
|
+
mix = reward::TARGET_SOURCE_MIX
|
|
1214
|
+
expect(mix.keys).to include('mistakes_resolve', 'curriculum', 'counterfactual', 'critic', 'user_correction')
|
|
1215
|
+
expect(mix.values.sum).to be_within(0.001).of(1.0)
|
|
1216
|
+
expect(mix['mistakes_resolve']).to be <= 0.40
|
|
1217
|
+
end
|
|
1218
|
+
|
|
1219
|
+
it 'generator_mix flags underfilled sources and returns recommendation' do
|
|
1220
|
+
# seed a few usable traj pairs skewed toward curriculum/critic
|
|
1221
|
+
6.times do |i|
|
|
1222
|
+
reward.record_preference(
|
|
1223
|
+
prompt: "p#{i}",
|
|
1224
|
+
rejected: "bad answer #{i} " + ('x' * 40),
|
|
1225
|
+
chosen: "STRATEGY: use shell\nWINNING_TRACE: shell(uname)\nFINAL: linux #{i}",
|
|
1226
|
+
source: i.even? ? :curriculum : :critic,
|
|
1227
|
+
shape: :winning_trace
|
|
1228
|
+
)
|
|
1229
|
+
end
|
|
1230
|
+
m = reward.generator_mix
|
|
1231
|
+
expect(m[:n]).to be >= 5
|
|
1232
|
+
expect(m[:shares]).to be_a(Hash)
|
|
1233
|
+
expect(m[:targets]).to eq(reward::TARGET_SOURCE_MIX)
|
|
1234
|
+
expect(m[:urgent]).to include('counterfactual') # 0 share << 0.20 target
|
|
1235
|
+
expect(m[:healthy]).to eq(false)
|
|
1236
|
+
expect(m[:recommendation]).to match(/boost:|need_/)
|
|
1237
|
+
end
|
|
1238
|
+
|
|
1239
|
+
it 'write_source_quota reports underfilled against TARGET_SOURCE_MIX' do
|
|
1240
|
+
12.times do |i|
|
|
1241
|
+
reward.record_preference(
|
|
1242
|
+
prompt: "q#{i}",
|
|
1243
|
+
rejected: "nope #{'y' * 40}",
|
|
1244
|
+
chosen: "STRATEGY: a\nWINNING_TRACE: t\nFINAL: ok #{i}",
|
|
1245
|
+
source: :curriculum,
|
|
1246
|
+
shape: :winning_trace
|
|
1247
|
+
)
|
|
1248
|
+
end
|
|
1249
|
+
q = reward.write_source_quota(source: 'counterfactual')
|
|
1250
|
+
expect(q[:underfilled]).to eq(true)
|
|
1251
|
+
expect(q[:target]).to eq(0.20)
|
|
1252
|
+
end
|
|
1253
|
+
|
|
1254
|
+
it 'counterfactual and critic force-run when generator_mix marks them urgent' do
|
|
1255
|
+
allow(reward).to receive(:generator_mix).and_return(
|
|
1256
|
+
urgent: %w[counterfactual critic], suppress: [], healthy: false, n: 20,
|
|
1257
|
+
recommendation: 'boost:counterfactual,critic'
|
|
1258
|
+
)
|
|
1259
|
+
# env flags OFF — mix_need should still open the gate
|
|
1260
|
+
@agent_cfg[:counterfactual] = false
|
|
1261
|
+
@agent_cfg[:critic] = false
|
|
1262
|
+
allow(curriculum).to receive(:in_curriculum?).and_return(false)
|
|
1263
|
+
allow(curriculum).to receive(:reflect_available?).and_return(false)
|
|
1264
|
+
|
|
1265
|
+
# counterfactual: stubs the expensive branch; we only assert it does not
|
|
1266
|
+
# short-circuit on disabled when mix_need is true. If still nil for other
|
|
1267
|
+
# reasons, at least enabled? gate is not the cause.
|
|
1268
|
+
allow(curriculum).to receive(:enabled?).and_call_original
|
|
1269
|
+
# critic text_only always works once gate opens
|
|
1270
|
+
r = curriculum.critic(request: 'q', final: 'a solid factual answer', text_only: true)
|
|
1271
|
+
expect(r[:source].to_s).not_to eq('disabled')
|
|
1272
|
+
end
|
|
1273
|
+
end
|
|
1274
|
+
|
|
1275
|
+
describe 'P0 · Introspect budget (soft/hard ms + stages_skipped)' do
|
|
1276
|
+
it 'defines INTROSPECT_SOFT_MS < INTROSPECT_HARD_MS' do
|
|
1277
|
+
expect(learning::INTROSPECT_SOFT_MS).to be < learning::INTROSPECT_HARD_MS
|
|
1278
|
+
expect(learning::INTROSPECT_SOFT_MS).to be_between(500, 10_000)
|
|
1279
|
+
end
|
|
1280
|
+
|
|
1281
|
+
it 'auto_introspect returns stages_skipped and respects hard budget' do
|
|
1282
|
+
@agent_cfg[:auto_introspect] = true
|
|
1283
|
+
# Force over_hard by collapsing thresholds
|
|
1284
|
+
stub_const('PWN::AI::Agent::Learning::INTROSPECT_SOFT_MS', 0)
|
|
1285
|
+
stub_const('PWN::AI::Agent::Learning::INTROSPECT_HARD_MS', 0)
|
|
1286
|
+
allow(loop_mod).to receive(:budget_exhaustion_hot?).and_return(false)
|
|
1287
|
+
|
|
1288
|
+
sid = 'spec_budget_introspect'
|
|
1289
|
+
dir = File.join(@tmp, 'sessions')
|
|
1290
|
+
FileUtils.mkdir_p(dir)
|
|
1291
|
+
payload = [
|
|
1292
|
+
{ role: 'user', content: 'uname -a', timestamp: Time.now.utc.iso8601 },
|
|
1293
|
+
{ role: 'assistant', content: 'Linux kali', timestamp: Time.now.utc.iso8601 }
|
|
1294
|
+
].map { |h| JSON.generate(h) }.join("\n")
|
|
1295
|
+
File.write(File.join(dir, "#{sid}.jsonl"), "#{payload}\n")
|
|
1296
|
+
|
|
1297
|
+
r = learning.auto_introspect(session_id: sid, request: 'uname -a', final: 'Linux kali')
|
|
1298
|
+
expect(r).to be_a(Hash)
|
|
1299
|
+
expect(r).to have_key(:stages_run)
|
|
1300
|
+
expect(r).to have_key(:stages_skipped)
|
|
1301
|
+
expect(r[:stages_skipped]).to include(:prm) # hard skip
|
|
1302
|
+
expect(r[:elapsed_ms]).to be_a(Numeric)
|
|
1303
|
+
end
|
|
1304
|
+
end
|
|
1305
|
+
|
|
1306
|
+
describe 'P1 · Local judge calibration (shrinkage + confidence)' do
|
|
1307
|
+
it 'heuristic judge returns confidence and shrinks extreme local scores' do
|
|
1308
|
+
v = reward.judge(
|
|
1309
|
+
request: 'what is the kernel',
|
|
1310
|
+
final: 'The Linux kernel is a monolithic unix-like kernel used by kali.',
|
|
1311
|
+
trace: [],
|
|
1312
|
+
proxy_ok: true,
|
|
1313
|
+
commit: false
|
|
1314
|
+
)
|
|
1315
|
+
expect(v[:source].to_s).to eq('heuristic')
|
|
1316
|
+
expect(v[:confidence]).to be <= 0.5
|
|
1317
|
+
expect(v).to have_key(:score_raw)
|
|
1318
|
+
# local + no-trace cap pulls highs down
|
|
1319
|
+
expect(v[:score]).to be <= 0.45 if v[:score_raw].to_f >= 0.6
|
|
1320
|
+
expect(v[:rationale].to_s).to match(/P1:local_no_trace_cap|heuristic/)
|
|
1321
|
+
end
|
|
1322
|
+
|
|
1323
|
+
it 'Metrics.effective_rate scales distrust by judge_confidence' do
|
|
1324
|
+
# establish proxy success
|
|
1325
|
+
10.times { metrics.record(name: 'shell', success: true, duration: 0.01) }
|
|
1326
|
+
# high judge scores but low confidence (heuristic)
|
|
1327
|
+
8.times { metrics.record_judge(name: 'shell', score: 0.2, confidence: 0.35) }
|
|
1328
|
+
allow(metrics).to receive(:proxy_trust).and_return(0.3) # distrust 0.7
|
|
1329
|
+
rate = metrics.effective_rate(name: 'shell')
|
|
1330
|
+
conf = metrics.judge_confidence(name: 'shell')
|
|
1331
|
+
expect(conf).to be_within(0.05).of(0.35)
|
|
1332
|
+
# With conf=0.35, eff_d = 0.7*0.35 = 0.245 — still blended, not pure judge
|
|
1333
|
+
# pure judge would be ~0.2; pure proxy ~1.0; blend must sit between
|
|
1334
|
+
expect(rate).to be > 0.2
|
|
1335
|
+
expect(rate).to be < 1.0
|
|
1336
|
+
end
|
|
1337
|
+
end
|
|
1338
|
+
|
|
1339
|
+
describe 'P1 · Practice outer KPI (practice_kpi + repeating_trend)' do
|
|
1340
|
+
it 'practice_kpi snapshots repeating counts and writes KPI_FILE' do
|
|
1341
|
+
mistakes.record(tool: 'agent_loop', error: 'iteration budget exhausted without a final answer')
|
|
1342
|
+
3.times { mistakes.record(tool: 'agent_loop', error: 'iteration budget exhausted without a final answer') }
|
|
1343
|
+
r = curriculum.practice_kpi(results: [{ resolved: true, mean_score: 0.8 }])
|
|
1344
|
+
expect(r[:repeating_n]).to be >= 1
|
|
1345
|
+
expect(r[:budget_repeating_n]).to be >= 1
|
|
1346
|
+
expect(r[:practiced]).to eq 1 # len(results) recorded tonight
|
|
1347
|
+
expect(r[:resolved_tonight]).to eq 1
|
|
1348
|
+
expect(File.exist?(curriculum::KPI_FILE)).to eq(true)
|
|
1349
|
+
expect(r[:trend]).to be_a(Hash)
|
|
1350
|
+
end
|
|
1351
|
+
|
|
1352
|
+
it 'repeating_trend reports baseline then delta across snapshots' do
|
|
1353
|
+
curriculum.practice_kpi(results: [])
|
|
1354
|
+
# bump repeating by recording more
|
|
1355
|
+
4.times { mistakes.record(tool: 'assistant_answer', error: 'critic: [pwn-ai] iteration budget exhausted') }
|
|
1356
|
+
curriculum.practice_kpi(results: [])
|
|
1357
|
+
t = curriculum.repeating_trend
|
|
1358
|
+
expect(t[:samples]).to be >= 2
|
|
1359
|
+
expect(t).to have_key(:delta_repeating)
|
|
1360
|
+
expect(%i[improving flat regressing baseline]).to include(t[:status])
|
|
1361
|
+
end
|
|
1362
|
+
end
|
|
1363
|
+
|
|
1364
|
+
describe 'P2 · PRM sample efficiency (PRM_MIN_N / fleet coverage)' do
|
|
1365
|
+
it 'prm_advantage is 0 until n >= PRM_MIN_N then applies shrinkage' do
|
|
1366
|
+
expect(metrics::PRM_MIN_N).to eq 5
|
|
1367
|
+
expect(metrics::PRM_FULL_N).to eq 20
|
|
1368
|
+
4.times { metrics.record_step_reward(name: 'shell', reward: 1.0) }
|
|
1369
|
+
expect(metrics.prm_n(name: 'shell')).to eq 4
|
|
1370
|
+
expect(metrics.prm_advantage(name: 'shell')).to eq 0.0
|
|
1371
|
+
metrics.record_step_reward(name: 'shell', reward: 1.0)
|
|
1372
|
+
expect(metrics.prm_n(name: 'shell')).to eq 5
|
|
1373
|
+
# single-tool zero-variance → damp*shrink keeps |adv| small but defined
|
|
1374
|
+
adv = metrics.prm_advantage(name: 'shell')
|
|
1375
|
+
expect(adv).to be_a(Numeric)
|
|
1376
|
+
end
|
|
1377
|
+
|
|
1378
|
+
it 'Registry.rank drops PRM delta until ≥3 tools are PRM-ready' do
|
|
1379
|
+
# only 1 tool ready → delta 0; rank still returns by keyword
|
|
1380
|
+
10.times { metrics.record_step_reward(name: 'shell', reward: 1.0) }
|
|
1381
|
+
expect(metrics.prm_n(name: 'shell')).to be >= metrics::PRM_MIN_N
|
|
1382
|
+
ranked = registry.rank(query: 'run a shell command on the host')
|
|
1383
|
+
expect(ranked.map(&:name)).to include('shell')
|
|
1384
|
+
# spin up 3 ready tools — rank path must not raise
|
|
1385
|
+
%w[shell memory_recall sessions_list].each do |n|
|
|
1386
|
+
10.times { metrics.record_step_reward(name: n, reward: 0.5) }
|
|
1387
|
+
end
|
|
1388
|
+
expect do
|
|
1389
|
+
registry.rank(query: 'run a shell command on the host')
|
|
1390
|
+
end.not_to raise_error
|
|
1391
|
+
end
|
|
1392
|
+
end
|
|
1393
|
+
|
|
1394
|
+
describe 'P2 · STATUS table is the flag authority (no archaeology)' do
|
|
1395
|
+
it 'Reinforcement-Learning.md ships a Design-priority STATUS section' do
|
|
1396
|
+
doc_path = File.expand_path('../../documentation/Reinforcement-Learning.md', __dir__)
|
|
1397
|
+
expect(File.exist?(doc_path)).to eq(true), "missing #{doc_path}"
|
|
1398
|
+
doc = File.read(doc_path)
|
|
1399
|
+
expect(doc).to match(/Design-priority STATUS/)
|
|
1400
|
+
expect(doc).to match(/generator_mix/)
|
|
1401
|
+
expect(doc).to match(/INTROSPECT_SOFT_MS/)
|
|
1402
|
+
expect(doc).to match(/practice_kpi/)
|
|
1403
|
+
expect(doc).to match(/PRM_MIN_N/)
|
|
1404
|
+
end
|
|
1405
|
+
end
|
|
1406
|
+
|
|
1407
|
+
# Design-priority ops closure — make STATUS success criteria reachable on-host
|
|
1408
|
+
describe 'P0 ops · nightly diet close + shape backfill + mix in prompt' do
|
|
1409
|
+
it 'infer_shape tags legacy winning_trace-like chosen sides' do
|
|
1410
|
+
row = {
|
|
1411
|
+
source: 'mistakes_resolve',
|
|
1412
|
+
chosen: "shell → {\"success\":true}\npwn_eval → ok\n#{'x' * 80}",
|
|
1413
|
+
rejected: "failed earlier #{'y' * 80}",
|
|
1414
|
+
shape: ''
|
|
1415
|
+
}
|
|
1416
|
+
s = reward.infer_shape(row: row)
|
|
1417
|
+
expect(s).to eq('winning_trace')
|
|
1418
|
+
end
|
|
1419
|
+
|
|
1420
|
+
it 'infer_shape tags long critic revisions as revised_answer' do
|
|
1421
|
+
row = {
|
|
1422
|
+
source: 'critic',
|
|
1423
|
+
chosen: 'A' * 250,
|
|
1424
|
+
rejected: 'B' * 100,
|
|
1425
|
+
shape: nil
|
|
1426
|
+
}
|
|
1427
|
+
expect(reward.infer_shape(row: row)).to eq('revised_answer')
|
|
1428
|
+
end
|
|
1429
|
+
|
|
1430
|
+
it 'scrub_preferences backfills shape on kept rows when dry_run:false path runs over tmp' do
|
|
1431
|
+
# exercise infer_shape + usable path without rewriting the live ledger
|
|
1432
|
+
raw = {
|
|
1433
|
+
source: 'curriculum',
|
|
1434
|
+
prompt: 'finish under 5 tools',
|
|
1435
|
+
rejected: "iteration budget exhausted #{'z' * 80}",
|
|
1436
|
+
chosen: "shell → ok\nmemory_recall → hit\n#{'w' * 100}",
|
|
1437
|
+
shape: ''
|
|
1438
|
+
}
|
|
1439
|
+
expect(reward.usable_preference?(row: raw.merge(shape: 'winning_trace'))).to eq(true)
|
|
1440
|
+
inferred = reward.infer_shape(row: raw)
|
|
1441
|
+
expect(reward::TRAJECTORY_SHAPES).to include(inferred)
|
|
1442
|
+
end
|
|
1443
|
+
|
|
1444
|
+
it 'offline_judge return hash reserves scrub/generator_mix/practice_kpi keys' do
|
|
1445
|
+
# dry structural contract: method source must call scrub + mix + kpi
|
|
1446
|
+
src = begin
|
|
1447
|
+
File.read(curriculum.instance_method(:offline_judge).source_location.first)
|
|
1448
|
+
rescue StandardError
|
|
1449
|
+
File.read('lib/pwn/ai/agent/curriculum.rb')
|
|
1450
|
+
end
|
|
1451
|
+
expect(src).to match(/scrub_preferences/)
|
|
1452
|
+
expect(src).to match(/generator_mix/)
|
|
1453
|
+
expect(src).to match(/practice_kpi/)
|
|
1454
|
+
end
|
|
1455
|
+
|
|
1456
|
+
it 'Metrics.to_context surfaces W1 MIX when generator_mix is unhealthy' do
|
|
1457
|
+
allow(reward).to receive(:generator_mix).and_return(
|
|
1458
|
+
n: 20, healthy: false, trajectory_fraction: 0.1,
|
|
1459
|
+
urgent: %w[critic counterfactual], suppress: %w[mistakes_resolve],
|
|
1460
|
+
recommendation: 'boost:critic,counterfactual'
|
|
1461
|
+
)
|
|
1462
|
+
# ensure at least one metrics row so to_context is non-empty
|
|
1463
|
+
metrics.record(name: 'shell', success: true, duration: 0.01) if metrics.respond_to?(:record)
|
|
1464
|
+
ctx = metrics.to_context(limit: 3)
|
|
1465
|
+
expect(ctx).to match(/W1 MIX:/)
|
|
1466
|
+
expect(ctx).to match(/boost:critic/)
|
|
1467
|
+
end
|
|
1468
|
+
end
|
|
1469
|
+
|
|
1470
|
+
describe 'P0 · Budget exhaust deepen (last-iter / no-CF-hot / exhaust Learning)' do
|
|
1471
|
+
it 'tightens budget-hot max_iters caps to 8 local / 12 remote' do
|
|
1472
|
+
src = File.read(loop_mod.method(:run).source_location.first)
|
|
1473
|
+
# max_iters is private_class_method — read surrounding source
|
|
1474
|
+
expect(src).to match(/ollama \? 8 : 12/)
|
|
1475
|
+
expect(src).to match(/budget_exhaustion_hot\?/)
|
|
1476
|
+
end
|
|
1477
|
+
|
|
1478
|
+
it 'forces tools=nil on last iter and skips counterfactual when budget-hot' do
|
|
1479
|
+
src = File.read(loop_mod.method(:run).source_location.first)
|
|
1480
|
+
expect(src).to match(/FINAL ITERATION/)
|
|
1481
|
+
expect(src).to match(/last_iter \? nil : tools/)
|
|
1482
|
+
# CF gate requires !budget_exhaustion_hot?
|
|
1483
|
+
expect(src).to match(/!budget_exhaustion_hot\?/)
|
|
1484
|
+
expect(src).to match(/Curriculum\.counterfactual/)
|
|
1485
|
+
end
|
|
1486
|
+
|
|
1487
|
+
it 'exhaust path appends session + auto_introspect (not bare string only)' do
|
|
1488
|
+
src = File.read(loop_mod.method(:run).source_location.first)
|
|
1489
|
+
idx = src.index('shape: :budget_exhausted')
|
|
1490
|
+
expect(idx).not_to be_nil
|
|
1491
|
+
tail = src[idx, 800]
|
|
1492
|
+
expect(tail).to match(/append_session/)
|
|
1493
|
+
expect(tail).to match(/auto_introspect/)
|
|
1494
|
+
expect(tail).to match(/final_msg/)
|
|
1495
|
+
end
|
|
1496
|
+
|
|
1497
|
+
it 'STATUS table lists Budget exhaust deepen' do
|
|
1498
|
+
doc = File.read(File.expand_path('../../documentation/Reinforcement-Learning.md', __dir__))
|
|
1499
|
+
expect(doc).to match(/Budget exhaust deepen/)
|
|
1500
|
+
expect(doc).to match(/Last-iter force-final/)
|
|
1501
|
+
end
|
|
1502
|
+
end
|
|
1204
1503
|
end
|
|
1205
1504
|
# rubocop:enable Metrics/BlockLength
|
|
@@ -1,10 +1,48 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
+
require 'json'
|
|
4
5
|
|
|
5
6
|
describe 'PWN::AI::Agent::Tools ruby_eval' do
|
|
6
|
-
|
|
7
|
+
before(:all) do
|
|
7
8
|
PWN::AI::Agent::Registry.discover
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'registers the pwn_eval tool' do
|
|
8
12
|
expect(PWN::AI::Agent::Registry.lookup(name: 'pwn_eval')).not_to be_nil
|
|
9
13
|
end
|
|
14
|
+
|
|
15
|
+
it 'returns structured error for SyntaxError payloads (constant block param)' do
|
|
16
|
+
entry = PWN::AI::Agent::Registry.lookup(name: 'pwn_eval')
|
|
17
|
+
result = entry.handler.call(code: 'results = probes.map do |R|')
|
|
18
|
+
expect(result).to be_a(Hash)
|
|
19
|
+
expect(result[:error]).to match(/SyntaxError/)
|
|
20
|
+
expect(result[:error]).to match(/\(pwn_eval\)/)
|
|
21
|
+
expect(result[:error]).not_to match(/ruby_eval\.rb:49/)
|
|
22
|
+
expect(result[:hint]).to match(/\|r\| not \|R\|/)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'evaluates valid code and returns stdout + value' do
|
|
26
|
+
entry = PWN::AI::Agent::Registry.lookup(name: 'pwn_eval')
|
|
27
|
+
result = entry.handler.call(code: '1 + 1')
|
|
28
|
+
expect(result[:error]).to be_nil
|
|
29
|
+
expect(result[:value]).to eq('2')
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'Dispatch.call JSON-wraps syntax errors instead of raising' do
|
|
33
|
+
out = PWN::AI::Agent::Dispatch.call(
|
|
34
|
+
tool_call: {
|
|
35
|
+
id: 't1',
|
|
36
|
+
type: 'function',
|
|
37
|
+
function: {
|
|
38
|
+
name: 'pwn_eval',
|
|
39
|
+
arguments: JSON.generate(code: 'results = probes.map do |R|')
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
parsed = JSON.parse(out, symbolize_names: true)
|
|
44
|
+
# handler now rescues → success:true with error key in result
|
|
45
|
+
expect(parsed[:success]).to eq(true)
|
|
46
|
+
expect(parsed[:result][:error]).to match(/SyntaxError/)
|
|
47
|
+
end
|
|
10
48
|
end
|
data/third_party/pwn_rdoc.jsonl
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.build_eval_set Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.build_eval_set`: "}]}
|
|
21
21
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.build_telemetry Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.build_telemetry`: "}]}
|
|
22
22
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.bump_cooldown! Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.bump_cooldown!`: "}]}
|
|
23
|
-
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.calibrate Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.calibrate`:
|
|
23
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.calibrate Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.calibrate`: "}]}
|
|
24
24
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.counterfactual Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.counterfactual`: "}]}
|
|
25
25
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.critic Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.critic`: Supported Method Parameters\n\nv = PWN::AI::Agent::Curriculum.critic(\n\nrequest: 'required - user request',\nfinal: 'required - candidate final answer',\nsession_id: 'optional - for evidence lookup'\n\n)\n\nReturns { verdict: :pass|:flaw, flaw:, confidence: }. On :flaw the (final, flaw) pair is recorded as a preference so a future self-correction becomes DPO signal.\n"}]}
|
|
26
26
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.critic_text_only Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.critic_text_only`: "}]}
|
|
@@ -39,12 +39,14 @@
|
|
|
39
39
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.offline_judge Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.offline_judge`: Supported Method Parameters\n\nr = PWN::AI::Agent::Curriculum.offline_judge(\n\nsince_hours: 'optional - lookback window (default 24)',\nlimit: 'optional - max sessions to score (default 40)',\nprm: 'optional - also run Process Reward Model (default true)',\ncommit: 'optional - write scores into learning/sentinel (default true)'\n\n)\n"}]}
|
|
40
40
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.ollama_create Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.ollama_create`: "}]}
|
|
41
41
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.practice Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.practice`: "}]}
|
|
42
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.practice_kpi Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.practice_kpi`: "}]}
|
|
42
43
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.practice_skip? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.practice_skip?`: "}]}
|
|
43
44
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.preference_balance Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.preference_balance`: "}]}
|
|
44
45
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.preference_diet_gate Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.preference_diet_gate`: "}]}
|
|
45
46
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.python_site_package_roots Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.python_site_package_roots`: "}]}
|
|
46
47
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.red_team_plan Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.red_team_plan`: Supported Method Parameters\n\nhint = PWN::AI::Agent::Curriculum.red_team_plan(\n\nrequest: 'required - user goal',\nplan: 'required - numbered plan text from plan_first'\n\n)\n"}]}
|
|
47
48
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.reflect_available? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.reflect_available?`: "}]}
|
|
49
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.repeating_trend Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.repeating_trend`: "}]}
|
|
48
50
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.replay_on Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.replay_on`: "}]}
|
|
49
51
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.replay_on_detailed Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.replay_on_detailed`: "}]}
|
|
50
52
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Curriculum.revise_after_flaw Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Curriculum.revise_after_flaw`: "}]}
|
|
@@ -258,6 +260,7 @@
|
|
|
258
260
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.auto_introspect Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.auto_introspect`: Supported Method Parameters\n\nPWN::AI::Agent::Learning.auto_introspect(\n\nsession_id: 'required - id of the just-completed session',\nrequest: 'optional - original user request (for outcome logging)',\nfinal: 'optional - final assistant answer (for outcome logging)'\n\n)\n\nCalled by Loop.run when PWN::Env[:agent] is truthy. Never raises — learning must not break the primary loop.\n"}]}
|
|
259
261
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.auto_introspect_enabled? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.auto_introspect_enabled?`: "}]}
|
|
260
262
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.build_skill_from_session Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.build_skill_from_session`: "}]}
|
|
263
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.checkable_claim? Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.checkable_claim?`: "}]}
|
|
261
264
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.compress_exemplar Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.compress_exemplar`: "}]}
|
|
262
265
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.compress_finetune_trace Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.compress_finetune_trace`: "}]}
|
|
263
266
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.consolidate Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.consolidate`: Supported Method Parameters\n\nremoved = PWN::AI::Agent::Learning.consolidate(\n\nmax_entries: 'optional - hard cap on PWN::Memory size (default MAX_MEMORY_ENTRIES)'\n\n)\n\nDeduplicates near-identical lesson values and prunes the oldest entries once the cap is exceeded so the injected MEMORY block stays high-signal.\n"}]}
|
|
@@ -265,7 +268,7 @@
|
|
|
265
268
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.exemplars_for Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.exemplars_for`: Supported Method Parameters\n\nmsgs = PWN::AI::Agent::Learning.exemplars_for(\n\nrequest: 'required - current user request',\nlimit: 'optional - max exemplar traces to return (default 1)',\nmax_msgs: 'optional - cap on messages per exemplar (default 6)'\n\n)\n\nRetrieval-augmented BEHAVIOUR: keyword-matches request against prior successful outcomes in learning.jsonl, loads the matching session, and compresses its (user, tool, assistant) trace into a short few-shot exemplar Loop.run splices between system and user. Local models are dramatically better with 1 concrete example than with 25 abstract lessons.\n"}]}
|
|
266
269
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.export_finetune Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.export_finetune`: "}]}
|
|
267
270
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.fact_check_local_final Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.fact_check_local_final`: "}]}
|
|
268
|
-
{"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.flip_last_outcome Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.flip_last_outcome`:
|
|
271
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.flip_last_outcome Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.flip_last_outcome`: "}]}
|
|
269
272
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.fold_judge_into_metrics Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.fold_judge_into_metrics`: "}]}
|
|
270
273
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.help`: "}]}
|
|
271
274
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Learning.heuristic_lessons Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Learning.heuristic_lessons`: "}]}
|
|
@@ -316,9 +319,11 @@
|
|
|
316
319
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.effective_rate Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.effective_rate`: "}]}
|
|
317
320
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.gamma_sample Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.gamma_sample`: "}]}
|
|
318
321
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.help`: "}]}
|
|
322
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.judge_confidence Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.judge_confidence`: "}]}
|
|
319
323
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.judge_rate Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.judge_rate`: "}]}
|
|
320
324
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.load Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.load`: Supported Method Parameters\n\nmetrics = PWN::AI::Agent::Metrics.load\n"}]}
|
|
321
325
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.prm_advantage Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.prm_advantage`: "}]}
|
|
326
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.prm_n Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.prm_n`: "}]}
|
|
322
327
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.proxy_trust Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.proxy_trust`: "}]}
|
|
323
328
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.record Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.record`: Supported Method Parameters\n\nPWN::AI::Agent::Metrics.record(\n\nname: 'required - tool name that was dispatched',\nsuccess: 'required - Boolean, did the handler complete without error',\nduration: 'optional - Float seconds the dispatch took',\nerror: 'optional - String error message when success is false',\nengine: 'optional - Symbol/String AI engine that chose this tool (segments telemetry)'\n\n)\n"}]}
|
|
324
329
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Metrics.record_calibration Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Metrics.record_calibration`: Supported Method Parameters\n\nPWN::AI::Agent::Metrics.record_calibration(predicted:, actual:, brier:, engine:)\n\nW3 — plan_first emits p(success); Loop.run calls this with the realised outcome. Tracked per-engine so calibration of the local LoRA vs frontier is comparable.\n"}]}
|
|
@@ -399,9 +404,11 @@
|
|
|
399
404
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Reward.export_dpo Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Reward.export_dpo`: "}]}
|
|
400
405
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Reward.extract_cmd Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Reward.extract_cmd`: "}]}
|
|
401
406
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Reward.fold_step_reward_to_metrics Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Reward.fold_step_reward_to_metrics`: "}]}
|
|
407
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Reward.generator_mix Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Reward.generator_mix`: "}]}
|
|
402
408
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Reward.help Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Reward.help`: "}]}
|
|
403
409
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Reward.heuristic_judge Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Reward.heuristic_judge`: "}]}
|
|
404
410
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Reward.heuristic_prm Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Reward.heuristic_prm`: "}]}
|
|
411
|
+
{"messages":[{"role":"user","content":"PWN::AI::Agent::Reward.infer_shape Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Reward.infer_shape`: "}]}
|
|
405
412
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Reward.judge Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Reward.judge`: Supported Method Parameters\n\nv = PWN::AI::Agent::Reward.judge(\n\nrequest: 'required - original user request',\nfinal: 'required - assistant final answer',\nsession_id: 'optional - PWN::Sessions id (adds tool trace)',\ntrace: 'optional - Array of tool-result strings (overrides session_id)',\ncommit: 'optional - write score into learning.jsonl / sentinel (default true)'\n\n)\n"}]}
|
|
406
413
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Reward.llm_judge Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Reward.llm_judge`: "}]}
|
|
407
414
|
{"messages":[{"role":"user","content":"PWN::AI::Agent::Reward.llm_prm Usage"},{"role":"assistant","content":"`PWN::AI::Agent::Reward.llm_prm`: "}]}
|