swarm_cli 3.0.0.alpha4 → 3.0.0.alpha5
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/lib/swarm_cli/v3/cli.rb +170 -3
- data/lib/swarm_cli/v3/command_completer.rb +2 -0
- data/lib/swarm_cli/v3/event_renderer.rb +8 -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: 6e35de7c5ee992cc18c100e24ea7448499422ea05b483fc5a422c5a8072af429
|
|
4
|
+
data.tar.gz: 920f46cc3531c44cc5aaa866d9c9daabe42abb77deec8eec2b34bca83a1f5816
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9ac7c114df87419e179bfeb7681ff213b512b10f0de3c331768ea3237f4dffe2cee7a60c10a7556c4df3e660fabf20432b228b441ef3773a1b2306df311f1818
|
|
7
|
+
data.tar.gz: d3681393fd04b53ec9ab1f1858b2a36d6ee3e01af54facefe84c8c589213eaa68337afe8415684ad3c8517684bf554d61fbc13fddfe9ea06e4a967f0f0cd14ea
|
data/lib/swarm_cli/v3/cli.rb
CHANGED
|
@@ -16,6 +16,9 @@ module SwarmCLI
|
|
|
16
16
|
HISTORY_SIZE = 1000
|
|
17
17
|
CONFIG_FILE = File.expand_path("~/.config/swarm/cli.json")
|
|
18
18
|
|
|
19
|
+
DEFAULT_REFINE_PROMPT = "Review what you just did. Identify weaknesses, errors, " \
|
|
20
|
+
"or areas for improvement, then make the improvements."
|
|
21
|
+
|
|
19
22
|
class << self
|
|
20
23
|
# Parse arguments and run the appropriate mode.
|
|
21
24
|
#
|
|
@@ -124,6 +127,10 @@ module SwarmCLI
|
|
|
124
127
|
@options[:defrag] = true
|
|
125
128
|
end
|
|
126
129
|
|
|
130
|
+
opts.on("--refine N", Integer, "Refine the response N additional times") do |n|
|
|
131
|
+
@options[:refine] = n
|
|
132
|
+
end
|
|
133
|
+
|
|
127
134
|
opts.on("--reboot-from PID", Integer, "Internal: consume reboot signal") do |pid|
|
|
128
135
|
@options[:reboot_from] = pid
|
|
129
136
|
end
|
|
@@ -190,7 +197,13 @@ module SwarmCLI
|
|
|
190
197
|
|
|
191
198
|
def run_prompt(agent)
|
|
192
199
|
expanded_prompt = expand_file_mentions(@options[:prompt])
|
|
193
|
-
|
|
200
|
+
|
|
201
|
+
if @options[:refine]
|
|
202
|
+
refine_with_display(agent, expanded_prompt, @options[:refine])
|
|
203
|
+
else
|
|
204
|
+
ask_with_display(agent, expanded_prompt)
|
|
205
|
+
end
|
|
206
|
+
|
|
194
207
|
check_and_reboot!(agent)
|
|
195
208
|
end
|
|
196
209
|
|
|
@@ -324,7 +337,7 @@ module SwarmCLI
|
|
|
324
337
|
puts ANSIColors.dim("\u{1F9E0} Memory: #{mem_status}")
|
|
325
338
|
puts ANSIColors.dim("Option+Enter for newline. Ctrl-C interrupts agent, double Ctrl-C quits.")
|
|
326
339
|
puts ANSIColors.dim("Type while agent is working to steer. /queue <prompt> for follow-up tasks.")
|
|
327
|
-
puts ANSIColors.dim("Commands: /clear /memory /defrag /queue /reboot /exit")
|
|
340
|
+
puts ANSIColors.dim("Commands: /help /clear /memory /defrag /queue /refine /reboot /exit")
|
|
328
341
|
puts
|
|
329
342
|
end
|
|
330
343
|
|
|
@@ -370,6 +383,19 @@ module SwarmCLI
|
|
|
370
383
|
@defrag_active = false
|
|
371
384
|
end
|
|
372
385
|
next
|
|
386
|
+
when ->(v) { v.is_a?(Array) && v[0] == :refine }
|
|
387
|
+
# [:refine, count, custom_prompt] - run interactive refinement
|
|
388
|
+
refine_count = cmd_result[1]
|
|
389
|
+
refine_prompt = cmd_result[2]
|
|
390
|
+
|
|
391
|
+
if agent.running? || agent_active
|
|
392
|
+
display.agent_print(ANSIColors.yellow("Agent is busy. Wait for it to finish before refining."))
|
|
393
|
+
else
|
|
394
|
+
agent_active = true
|
|
395
|
+
ctx = { agent: agent, display: display, reader: reader, count: refine_count, custom_prompt: refine_prompt }
|
|
396
|
+
dispatch_refine(reactor, ctx) { agent_active = false }
|
|
397
|
+
end
|
|
398
|
+
next
|
|
373
399
|
when Array
|
|
374
400
|
# [:process, prompt] - command wants us to process this prompt
|
|
375
401
|
line = cmd_result[1]
|
|
@@ -583,12 +609,133 @@ module SwarmCLI
|
|
|
583
609
|
ask_with_display_sync(agent, merged_prompt, display)
|
|
584
610
|
end
|
|
585
611
|
|
|
612
|
+
# ----------------------------------------------------------------
|
|
613
|
+
# Refinement
|
|
614
|
+
# ----------------------------------------------------------------
|
|
615
|
+
|
|
616
|
+
# Dispatch an interactive refine task asynchronously.
|
|
617
|
+
#
|
|
618
|
+
# @param reactor [Async::Reactor] the async reactor
|
|
619
|
+
# @param context [Hash] contains :agent, :display, :reader, :count, :custom_prompt
|
|
620
|
+
# @yield Called in ensure block to reset agent_active flag
|
|
621
|
+
# @return [void]
|
|
622
|
+
def dispatch_refine(reactor, context)
|
|
623
|
+
reactor.async do
|
|
624
|
+
refine_with_display_sync(context[:agent], context[:count], context[:custom_prompt], context[:display])
|
|
625
|
+
check_and_reboot_sync!(context[:agent], context[:display], context[:reader])
|
|
626
|
+
rescue StandardError => e
|
|
627
|
+
context[:display].agent_print(ANSIColors.red("Error: #{e.class}: #{e.message}"))
|
|
628
|
+
ensure
|
|
629
|
+
yield
|
|
630
|
+
end
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
# Run iterative refinement in non-interactive mode.
|
|
634
|
+
#
|
|
635
|
+
# Uses agent.loop() with max_iterations = refine_count + 1 (the first
|
|
636
|
+
# iteration IS the user's prompt, subsequent iterations use the default
|
|
637
|
+
# refinement prompt). Only the final response is printed.
|
|
638
|
+
#
|
|
639
|
+
# @param agent [SwarmSDK::V3::Agent] the agent
|
|
640
|
+
# @param prompt [String] the initial prompt
|
|
641
|
+
# @param refine_count [Integer] number of additional refinement iterations
|
|
642
|
+
# @return [void]
|
|
643
|
+
def refine_with_display(agent, prompt, refine_count)
|
|
644
|
+
result = agent.loop(
|
|
645
|
+
kickoff: prompt,
|
|
646
|
+
iterate: DEFAULT_REFINE_PROMPT,
|
|
647
|
+
max_iterations: refine_count + 1,
|
|
648
|
+
converge: false,
|
|
649
|
+
) do |event|
|
|
650
|
+
case event[:type]
|
|
651
|
+
when "content_chunk"
|
|
652
|
+
# Suppress intermediate content — we'll print the final response
|
|
653
|
+
nil
|
|
654
|
+
else
|
|
655
|
+
line = EventRenderer.format(event)
|
|
656
|
+
puts line if line
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
return unless result&.final_response
|
|
661
|
+
|
|
662
|
+
puts
|
|
663
|
+
puts ANSIColors.cyan("#{agent.name}> ")
|
|
664
|
+
puts TTY::Markdown.parse(result.final_response.content.to_s)
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
# Run iterative refinement in interactive mode.
|
|
668
|
+
#
|
|
669
|
+
# Uses agent.loop() with the existing conversation context (STM).
|
|
670
|
+
# Each iteration streams content and renders markdown. The iterate
|
|
671
|
+
# prompt is either the custom text or the default refinement prompt.
|
|
672
|
+
#
|
|
673
|
+
# @param agent [SwarmSDK::V3::Agent] the agent
|
|
674
|
+
# @param count [Integer] number of refinement iterations
|
|
675
|
+
# @param custom_prompt [String, nil] custom refinement prompt (nil = default)
|
|
676
|
+
# @param display [Display] the display coordinator
|
|
677
|
+
# @return [void]
|
|
678
|
+
def refine_with_display_sync(agent, count, custom_prompt, display)
|
|
679
|
+
iterate_prompt = custom_prompt || DEFAULT_REFINE_PROMPT
|
|
680
|
+
|
|
681
|
+
display.start_working
|
|
682
|
+
full_content = +""
|
|
683
|
+
output_chars = 0
|
|
684
|
+
|
|
685
|
+
begin
|
|
686
|
+
agent.loop(
|
|
687
|
+
kickoff: iterate_prompt,
|
|
688
|
+
iterate: iterate_prompt,
|
|
689
|
+
max_iterations: count,
|
|
690
|
+
converge: false,
|
|
691
|
+
) do |event|
|
|
692
|
+
case event[:type]
|
|
693
|
+
when "content_chunk"
|
|
694
|
+
full_content << event[:content] if event[:content]
|
|
695
|
+
output_chars += event[:content].length if event[:content]
|
|
696
|
+
display.update_activity("\u{2193} ~#{output_chars / 4} tokens")
|
|
697
|
+
when "loop_iteration_completed"
|
|
698
|
+
# Render accumulated content after each iteration
|
|
699
|
+
unless full_content.empty?
|
|
700
|
+
rendered = TTY::Markdown.parse(full_content)
|
|
701
|
+
display.agent_print("\n#{ANSIColors.cyan("#{agent.name}> ")}\n#{rendered}")
|
|
702
|
+
full_content.clear
|
|
703
|
+
output_chars = 0
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
ensure
|
|
708
|
+
display.stop_working
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
# Render any remaining content from the last iteration
|
|
712
|
+
return if full_content.empty?
|
|
713
|
+
|
|
714
|
+
rendered = TTY::Markdown.parse(full_content)
|
|
715
|
+
display.agent_print("\n#{ANSIColors.cyan("#{agent.name}> ")}\n#{rendered}")
|
|
716
|
+
end
|
|
717
|
+
|
|
586
718
|
# ----------------------------------------------------------------
|
|
587
719
|
# Slash commands (interactive mode)
|
|
588
720
|
# ----------------------------------------------------------------
|
|
589
721
|
|
|
722
|
+
# Help text for all available slash commands.
|
|
723
|
+
COMMAND_HELP = [
|
|
724
|
+
["/help", "Show this help message"],
|
|
725
|
+
["/clear", "Clear conversation and queue (memory preserved)"],
|
|
726
|
+
["/memory", "Show memory and token stats"],
|
|
727
|
+
["/defrag", "Run memory defragmentation"],
|
|
728
|
+
["/queue <prompt>", "Queue a follow-up prompt for after the current task"],
|
|
729
|
+
["/refine [N] [prompt]", "Refine the last response N times (default: 3)"],
|
|
730
|
+
["/reboot", "Restart the process to reload code changes"],
|
|
731
|
+
["/exit, /quit", "Exit the chat"],
|
|
732
|
+
].freeze
|
|
733
|
+
|
|
590
734
|
def handle_slash_command(input, agent, display, reader, agent_active = false)
|
|
591
735
|
case input
|
|
736
|
+
when "/help"
|
|
737
|
+
show_help(display)
|
|
738
|
+
true
|
|
592
739
|
when "/clear"
|
|
593
740
|
agent.clear
|
|
594
741
|
@follow_up_queue&.clear
|
|
@@ -626,6 +773,10 @@ module SwarmCLI
|
|
|
626
773
|
when "/queue"
|
|
627
774
|
display.agent_print(ANSIColors.red("Usage: /queue <prompt>"))
|
|
628
775
|
true
|
|
776
|
+
when %r{^/refine(?:\s+(\d+))?(?:\s+(.+))?$}
|
|
777
|
+
count = (Regexp.last_match(1) || 3).to_i
|
|
778
|
+
custom_prompt = Regexp.last_match(2)
|
|
779
|
+
[:refine, count, custom_prompt]
|
|
629
780
|
when %r{^/([a-zA-Z0-9_-]+)(?:\s+(.+))?}
|
|
630
781
|
# Check if this is a skill invocation (supports hyphens in names)
|
|
631
782
|
skill_name = Regexp.last_match(1)
|
|
@@ -642,7 +793,7 @@ module SwarmCLI
|
|
|
642
793
|
else
|
|
643
794
|
# Unknown command
|
|
644
795
|
display.agent_print(ANSIColors.red("Unknown command: #{input}"))
|
|
645
|
-
display.agent_print(ANSIColors.dim("
|
|
796
|
+
display.agent_print(ANSIColors.dim("Type /help for available commands."))
|
|
646
797
|
true
|
|
647
798
|
end
|
|
648
799
|
else
|
|
@@ -650,6 +801,22 @@ module SwarmCLI
|
|
|
650
801
|
end
|
|
651
802
|
end
|
|
652
803
|
|
|
804
|
+
# Display available commands with descriptions.
|
|
805
|
+
#
|
|
806
|
+
# @param display [Display] the display coordinator
|
|
807
|
+
# @return [void]
|
|
808
|
+
def show_help(display)
|
|
809
|
+
max_cmd = COMMAND_HELP.map { |cmd, _| cmd.length }.max
|
|
810
|
+
lines = [ANSIColors.cyan("--- Available Commands ---")]
|
|
811
|
+
COMMAND_HELP.each do |cmd, desc|
|
|
812
|
+
lines << " #{ANSIColors.bold(cmd.ljust(max_cmd))} #{desc}"
|
|
813
|
+
end
|
|
814
|
+
lines << ""
|
|
815
|
+
lines << ANSIColors.dim("Option+Enter for newline. Ctrl-C interrupts agent, double Ctrl-C quits.")
|
|
816
|
+
lines << ANSIColors.dim("Type while agent is working to steer. Use @file to include file contents.")
|
|
817
|
+
display.agent_print(lines.join("\n"))
|
|
818
|
+
end
|
|
819
|
+
|
|
653
820
|
def show_memory_stats_sync(agent, display)
|
|
654
821
|
unless agent.definition.memory_enabled?
|
|
655
822
|
display.agent_print(ANSIColors.yellow("Memory not enabled for this agent."))
|
|
@@ -64,6 +64,14 @@ module SwarmCLI
|
|
|
64
64
|
ANSIColors.cyan(" \u{1F9F9} Starting defrag (#{event[:total_cards]} cards, #{event[:total_clusters]} clusters)")
|
|
65
65
|
when "memory_defrag_complete"
|
|
66
66
|
ANSIColors.green(" \u{2705} Defrag complete in #{event[:elapsed_ms]}ms")
|
|
67
|
+
when "loop_started"
|
|
68
|
+
ANSIColors.cyan(" \u{1F504} Starting refinement (max #{event[:max_iterations]} iterations)")
|
|
69
|
+
when "loop_iteration_completed"
|
|
70
|
+
delta = event[:delta_score] ? " (similarity: #{(event[:delta_score] * 100).round(1)}%)" : ""
|
|
71
|
+
ANSIColors.dim(" \u{1F504} Iteration #{event[:iteration]} complete#{delta}")
|
|
72
|
+
when "loop_completed"
|
|
73
|
+
label = event[:converged] ? "converged" : "#{event[:iterations]} iterations"
|
|
74
|
+
ANSIColors.green(" \u{2705} Refinement complete (#{label})")
|
|
67
75
|
end
|
|
68
76
|
end
|
|
69
77
|
|