pwn 0.5.759 → 0.5.761
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/Gemfile +1 -1
- data/documentation/pwn-ai-Agent.md +44 -10
- data/etc/default_skills/pwn/banner/SKILL.md +1 -1
- data/lib/pwn/ai/agent/loop.rb +73 -18
- data/lib/pwn/banner.rb +740 -81
- data/lib/pwn/plugins/repl/ai_console.rb +97 -10
- data/lib/pwn/plugins/repl/ai_swarm.rb +1 -0
- data/lib/pwn/version.rb +1 -1
- data/spec/lib/pwn/ai/agent/loop_spec.rb +1 -1
- data/spec/lib/pwn/banner_spec.rb +523 -19
- data/spec/lib/pwn/plugins/repl/ai_console_spec.rb +267 -5
- data/third_party/pwn_rdoc.jsonl +36 -3
- metadata +3 -3
|
@@ -86,6 +86,7 @@ module PWN
|
|
|
86
86
|
|
|
87
87
|
def stop
|
|
88
88
|
@stopping = true
|
|
89
|
+
request_model_cancel
|
|
89
90
|
submit('Stop the active request.')
|
|
90
91
|
end
|
|
91
92
|
|
|
@@ -104,6 +105,8 @@ module PWN
|
|
|
104
105
|
|
|
105
106
|
# Read a duplicated terminal rather than stdin, which belongs to tools.
|
|
106
107
|
class Keyboard
|
|
108
|
+
attr_writer :interval
|
|
109
|
+
|
|
107
110
|
KEYS = {
|
|
108
111
|
"\e[A" => :up, "\e[B" => :down, "\e[C" => :right, "\e[D" => :left,
|
|
109
112
|
"\eOA" => :up, "\eOB" => :down, "\eOC" => :right, "\eOD" => :left,
|
|
@@ -119,7 +122,7 @@ module PWN
|
|
|
119
122
|
end
|
|
120
123
|
|
|
121
124
|
def call
|
|
122
|
-
if @source.wait_readable(@buffer.empty? ? 0.04 : 0)
|
|
125
|
+
if @source.wait_readable(@buffer.empty? ? (@interval || 0.04) : 0)
|
|
123
126
|
chunk = @source.read_nonblock(4096, exception: false)
|
|
124
127
|
return :eof if chunk.nil? && @buffer.empty?
|
|
125
128
|
|
|
@@ -401,6 +404,7 @@ module PWN
|
|
|
401
404
|
break if @leaving && !busy?
|
|
402
405
|
|
|
403
406
|
draw
|
|
407
|
+
@getch.interval = @focus == :animation && %i[asteroids galaga frogger].include?(@banner_name) ? 0.01 : 0.04 if @getch.is_a?(Keyboard)
|
|
404
408
|
handle(@getch.call)
|
|
405
409
|
rescue Interrupt
|
|
406
410
|
cancel
|
|
@@ -524,13 +528,12 @@ module PWN
|
|
|
524
528
|
|
|
525
529
|
def cancel
|
|
526
530
|
if busy?
|
|
527
|
-
|
|
528
|
-
|
|
531
|
+
first = !@cancelling
|
|
529
532
|
@cancelling = true
|
|
530
533
|
@control&.stop
|
|
531
534
|
@swarm&.cancel_all
|
|
532
535
|
@tool_input.close unless @tool_input.closed?
|
|
533
|
-
add(:warning, 'Cancellation requested. Waiting for
|
|
536
|
+
add(:warning, 'Cancellation requested. Waiting for a safe boundary; work already started is not undone.') if first
|
|
534
537
|
else
|
|
535
538
|
@editor.replace('')
|
|
536
539
|
end
|
|
@@ -681,6 +684,7 @@ module PWN
|
|
|
681
684
|
def handle(key)
|
|
682
685
|
return if key.nil?
|
|
683
686
|
return handle_system_role(key) if @role_editor && (!@too_small || ["\e", "\u0003", "\u0004", :eof].include?(key))
|
|
687
|
+
return cancel if key == "\u0003" && busy?
|
|
684
688
|
|
|
685
689
|
return if @too_small && ![:eof, "\u0003", "\u0004"].include?(key)
|
|
686
690
|
return cancel if @too_small && key == "\u0003" && !@model_prompt
|
|
@@ -689,7 +693,7 @@ module PWN
|
|
|
689
693
|
|
|
690
694
|
return clear_view if key == "\u000c"
|
|
691
695
|
|
|
692
|
-
if
|
|
696
|
+
if key == "\u0013" || (key == "\u0007" && (@focus != :animation || @workspace))
|
|
693
697
|
@workspace ? @workspace = nil : open_swarm
|
|
694
698
|
return
|
|
695
699
|
end
|
|
@@ -714,7 +718,10 @@ module PWN
|
|
|
714
718
|
|
|
715
719
|
if ["\u0018", "\u0014"].include?(key)
|
|
716
720
|
@menu = nil
|
|
717
|
-
|
|
721
|
+
panes = @header_pane_height ? %i[mission session animation] : %i[mission session]
|
|
722
|
+
@focus = panes[(panes.index(@focus).to_i + 1) % panes.length]
|
|
723
|
+
@banner_game&.controls
|
|
724
|
+
@banner_game_tick = nil
|
|
718
725
|
return
|
|
719
726
|
end
|
|
720
727
|
|
|
@@ -726,6 +733,8 @@ module PWN
|
|
|
726
733
|
@menu = nil
|
|
727
734
|
return
|
|
728
735
|
end
|
|
736
|
+
return handle_animation(key) if @focus == :animation && ![:eof, "\u0003", "\u0004"].include?(key)
|
|
737
|
+
|
|
729
738
|
case key
|
|
730
739
|
when :eof, "\u0004"
|
|
731
740
|
@leaving = true
|
|
@@ -794,6 +803,34 @@ module PWN
|
|
|
794
803
|
end
|
|
795
804
|
end
|
|
796
805
|
|
|
806
|
+
def handle_animation(key)
|
|
807
|
+
if key == "\u0007"
|
|
808
|
+
names = PWN::Banner.mini_names
|
|
809
|
+
@banner_name = names[(names.index(@banner_name).to_i + 1) % names.length]
|
|
810
|
+
@banner_frame_seconds = PWN::Banner.mini_frame_seconds(name: @banner_name)
|
|
811
|
+
@banner_started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
812
|
+
@banner_game = @banner_game_key = @banner_game_tick = nil
|
|
813
|
+
elsif key == :paste_start
|
|
814
|
+
@focus = :mission
|
|
815
|
+
@paste = true
|
|
816
|
+
@banner_game&.controls
|
|
817
|
+
elsif [:up, :down, :left, :right, ' '].include?(key)
|
|
818
|
+
return if %i[asteroids galaga frogger].include?(@banner_name) && @banner_game_frame&.dig(:state, :explosion).to_i.positive?
|
|
819
|
+
|
|
820
|
+
@banner_game&.key(key)
|
|
821
|
+
# Paint a discrete control immediately, not after the next idle tick.
|
|
822
|
+
@banner_game_tick = nil if %i[asteroids galaga frogger].include?(@banner_name)
|
|
823
|
+
elsif ["\r", "\n", "\e", :newline].include?(key)
|
|
824
|
+
@focus = :mission
|
|
825
|
+
@banner_game&.controls
|
|
826
|
+
elsif key.is_a?(String) && key.ord >= 32
|
|
827
|
+
@focus = :mission
|
|
828
|
+
@banner_game&.controls
|
|
829
|
+
@editor.insert(key)
|
|
830
|
+
refresh_completion
|
|
831
|
+
end
|
|
832
|
+
end
|
|
833
|
+
|
|
797
834
|
def open_system_role
|
|
798
835
|
return add(:warning, 'System role cannot change while a request or swarm job is running.') if busy?
|
|
799
836
|
|
|
@@ -1029,7 +1066,7 @@ module PWN
|
|
|
1029
1066
|
['REQUEST', state, "Elapsed #{elapsed}", "Completed tools #{@completed_tools}",
|
|
1030
1067
|
"Events #{@event_count}", '', 'TOKENS', "#{usage[:input_tokens]} in · #{usage[:output_tokens]} out",
|
|
1031
1068
|
"cached #{usage[:cached_tokens]} · calls #{usage[:calls]}", "Cost #{cost}",
|
|
1032
|
-
'', 'SWARM', swarm[:swarm_id] || 'none', "#{active} active · ^
|
|
1069
|
+
'', 'SWARM', swarm[:swarm_id] || 'none', "#{active} active · ^S swarm",
|
|
1033
1070
|
'', 'VIEW', @verbose ? 'verbose' : 'compact · /verbose', scroll_status,
|
|
1034
1071
|
'', 'LAST TOOL', @last_tool || 'not observed']
|
|
1035
1072
|
end
|
|
@@ -1201,6 +1238,8 @@ module PWN
|
|
|
1201
1238
|
end
|
|
1202
1239
|
cadence = cells ? @banner_frame_seconds : PWN::Banner::MINI_FRAME_SECONDS
|
|
1203
1240
|
frame = ((now - @banner_started) / cadence).floor % PWN::Banner::MINI_FRAME_COUNT
|
|
1241
|
+
return playable_banner(size, now) if cells && @focus == :animation
|
|
1242
|
+
return automatic_banner(size, now) if cells && size >= 5 && %i[pacman galaga frogger].include?(@banner_name)
|
|
1204
1243
|
return PWN::Banner.mini_cells(name: @banner_name, frame: frame, width: size, height: size, seed: @banner_seed, branding: false) if cells
|
|
1205
1244
|
|
|
1206
1245
|
art = PWN::Banner.mini_frame(name: @banner_name, frame: frame, width: size, height: size, branding: false)
|
|
@@ -1210,6 +1249,43 @@ module PWN
|
|
|
1210
1249
|
canvas
|
|
1211
1250
|
end
|
|
1212
1251
|
|
|
1252
|
+
def automatic_banner(size, now)
|
|
1253
|
+
key = [@banner_session, @banner_name, size, @banner_seed]
|
|
1254
|
+
if @banner_demo_key != key
|
|
1255
|
+
@banner_demo_key = key
|
|
1256
|
+
@banner_demo = PWN::Banner::MiniGame.new(name: @banner_name, width: size, height: size, seed: @banner_seed, demo: true)
|
|
1257
|
+
@banner_demo.step
|
|
1258
|
+
@banner_demo_tick = nil
|
|
1259
|
+
end
|
|
1260
|
+
tick = (now / @banner_frame_seconds).floor
|
|
1261
|
+
if @banner_demo_tick != tick
|
|
1262
|
+
# Advance one frame, never build 1800 or catch up after a modal.
|
|
1263
|
+
@banner_demo_frame = @banner_demo.step
|
|
1264
|
+
@banner_demo_cells = @banner_demo.cells(@banner_demo_frame)
|
|
1265
|
+
@banner_demo_tick = tick
|
|
1266
|
+
end
|
|
1267
|
+
@banner_demo_cells
|
|
1268
|
+
end
|
|
1269
|
+
|
|
1270
|
+
def playable_banner(size, now)
|
|
1271
|
+
key = [@banner_session, @banner_name, size, @banner_seed]
|
|
1272
|
+
if @banner_game_key != key
|
|
1273
|
+
@banner_game_key = key
|
|
1274
|
+
@banner_game = PWN::Banner::MiniGame.new(name: @banner_name, width: size, height: size, seed: @banner_seed)
|
|
1275
|
+
@banner_game_tick = nil
|
|
1276
|
+
end
|
|
1277
|
+
# Human Snake input needs a full reaction interval on its tiny board;
|
|
1278
|
+
# decorative replay cadence is independent of player movement.
|
|
1279
|
+
cadence = @banner_name == :snake ? 0.2 : @banner_frame_seconds
|
|
1280
|
+
tick = (now / cadence).floor
|
|
1281
|
+
if @banner_game_tick != tick
|
|
1282
|
+
@banner_game_frame = @banner_game.step
|
|
1283
|
+
@banner_game_cells = @banner_game.cells(@banner_game_frame)
|
|
1284
|
+
@banner_game_tick = tick
|
|
1285
|
+
end
|
|
1286
|
+
@banner_game_cells
|
|
1287
|
+
end
|
|
1288
|
+
|
|
1213
1289
|
def draw_banner(size)
|
|
1214
1290
|
cells = banner_frame(size, cells: true)
|
|
1215
1291
|
top = [(size - cells.length) / 2, 0].max
|
|
@@ -1267,6 +1343,12 @@ module PWN
|
|
|
1267
1343
|
|
|
1268
1344
|
def footer_text
|
|
1269
1345
|
text = '/ menu · ^C=cancel · ^D=back · ^L=clear · ^T=toggle pane · ^S=swarm · ^R=search ^O=operations · ↑↓ history · Shift+Enter=newline · Enter=send'
|
|
1346
|
+
if @focus == :animation
|
|
1347
|
+
controls = { pacman: '↑↓←→ start/steer · ● power', snake: '↑↓←→ start/steer', falling_blocks: '←→ move · ↓ faster · Space=rotate',
|
|
1348
|
+
asteroids: '←→ turn · ↑↓ thrust/reverse · Space=fire', galaga: '↑↓←→ move · Space=fire',
|
|
1349
|
+
frogger: '↑↓←→ hop · logs → goals' }.fetch(@banner_name, '↑↓←→ play')
|
|
1350
|
+
return text.sub('↑↓ history', "^G=next game · #{controls}").sub('Shift+Enter=newline · Enter=send', 'Enter/Esc=draft')
|
|
1351
|
+
end
|
|
1270
1352
|
@focus == :session ? text.sub('↑↓ history', '↑↓ scroll · HOME · PGUP · PGDN · END') : text
|
|
1271
1353
|
end
|
|
1272
1354
|
|
|
@@ -1295,6 +1377,7 @@ module PWN
|
|
|
1295
1377
|
visible.each_with_index { |spans, index| put_spans(2 + index, @header_text_column, spans) }
|
|
1296
1378
|
if @header_pane_height
|
|
1297
1379
|
box(0, 0, height, height, '')
|
|
1380
|
+
mark_active(0, 0, height, 'PLAY') if @focus == :animation
|
|
1298
1381
|
draw_banner(height - 2)
|
|
1299
1382
|
end
|
|
1300
1383
|
height
|
|
@@ -1338,6 +1421,8 @@ module PWN
|
|
|
1338
1421
|
end
|
|
1339
1422
|
engine = PWN::Env.dig(:ai, :active).to_s
|
|
1340
1423
|
model = REPL.pwn_ai_engine_model(engine: engine)
|
|
1424
|
+
header_layout(engine, model)
|
|
1425
|
+
@focus = :mission if @focus == :animation && !@header_pane_height
|
|
1341
1426
|
footer, compose_height, footer_height = layout_chrome
|
|
1342
1427
|
@compose_height = compose_height
|
|
1343
1428
|
@footer_height = footer_height
|
|
@@ -1359,9 +1444,9 @@ module PWN
|
|
|
1359
1444
|
rows.slice(start, @page_size).to_a.each_with_index { |(color, row), index| put(header_height + 1 + index, 2, row, color) }
|
|
1360
1445
|
draw_sidebar(header_height, pane_width + 1, timeline_height) if sidebar.positive?
|
|
1361
1446
|
compose_y = @height - compose_height - footer_height
|
|
1362
|
-
mission_title = @focus == :
|
|
1447
|
+
mission_title = @focus == :mission ? 'MISSION CONTROL · active' : 'MISSION CONTROL'
|
|
1363
1448
|
box(compose_y, 0, compose_height, @width - 1, mission_title)
|
|
1364
|
-
mark_active(compose_y, 0, @width - 1, mission_title) if @focus
|
|
1449
|
+
mark_active(compose_y, 0, @width - 1, mission_title) if @focus == :mission
|
|
1365
1450
|
draw_composer(compose_y, height: compose_height)
|
|
1366
1451
|
footer_top = @height - footer_height
|
|
1367
1452
|
footer.first(footer_height).each_with_index { |line, index| put(footer_top + index, 1, line, tone(:footer)) }
|
|
@@ -1370,7 +1455,9 @@ module PWN
|
|
|
1370
1455
|
draw_details(engine, model) if @details
|
|
1371
1456
|
draw_swarm if @workspace
|
|
1372
1457
|
draw_system_role if @role_editor
|
|
1373
|
-
if @focus == :
|
|
1458
|
+
if @focus == :animation && !(@details || @workspace || @role_editor || @model_prompt)
|
|
1459
|
+
@screen.setpos(1, 1)
|
|
1460
|
+
elsif @focus == :session && !(@details || @workspace || @role_editor)
|
|
1374
1461
|
@screen.setpos(header_height + 1, 2)
|
|
1375
1462
|
elsif @role_editor || !(@details || @workspace)
|
|
1376
1463
|
@screen.setpos(*@cursor_position)
|
data/lib/pwn/version.rb
CHANGED
|
@@ -228,7 +228,7 @@ describe PWN::AI::Agent::Loop do # rubocop:disable Metrics/BlockLength
|
|
|
228
228
|
calls = []
|
|
229
229
|
owner = Thread.current
|
|
230
230
|
allow(described_class).to receive(:call_engine) do |opts|
|
|
231
|
-
expect(Thread.current).
|
|
231
|
+
expect(Thread.current).not_to eq(owner)
|
|
232
232
|
calls << Marshal.load(Marshal.dump(opts[:messages]))
|
|
233
233
|
if calls.length == 1
|
|
234
234
|
entered << true
|