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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a4a46dc28301d9944d4780989720a455736cdd45f5f770c7ac1b3d0d44e4740e
4
- data.tar.gz: daf3e6f7887eef22b502e74da0c527f19c10c99e4a99da7441367cc131ad62bc
3
+ metadata.gz: 6e35de7c5ee992cc18c100e24ea7448499422ea05b483fc5a422c5a8072af429
4
+ data.tar.gz: 920f46cc3531c44cc5aaa866d9c9daabe42abb77deec8eec2b34bca83a1f5816
5
5
  SHA512:
6
- metadata.gz: 2a9f88d5825f7c5d2ac65d5c891d5f44c35902fb54ba1caf691c8a79a22ecccce5957a6c8e03144ab51077f34b02c440afd6f79ff596f27cdddef5a1c30aad10
7
- data.tar.gz: c77b804edb2b3c2f348630424ad803d4d1da6d047f0717ba74d593f3989f0a0fe854407163feab417bf7816a96b37fafc852e23133bfc3789becd5e0653e9d17
6
+ metadata.gz: 9ac7c114df87419e179bfeb7681ff213b512b10f0de3c331768ea3237f4dffe2cee7a60c10a7556c4df3e660fabf20432b228b441ef3773a1b2306df311f1818
7
+ data.tar.gz: d3681393fd04b53ec9ab1f1858b2a36d6ee3e01af54facefe84c8c589213eaa68337afe8415684ad3c8517684bf554d61fbc13fddfe9ea06e4a967f0f0cd14ea
@@ -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
- ask_with_display(agent, expanded_prompt)
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("Commands: /clear /memory /defrag /queue /reboot /exit"))
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."))
@@ -19,10 +19,12 @@ module SwarmCLI
19
19
  class CommandCompleter
20
20
  # Available slash commands
21
21
  COMMANDS = [
22
+ "/help",
22
23
  "/clear",
23
24
  "/memory",
24
25
  "/defrag",
25
26
  "/queue",
27
+ "/refine",
26
28
  "/reboot",
27
29
  "/exit",
28
30
  "/quit",
@@ -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
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: swarm_cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0.alpha4
4
+ version: 3.0.0.alpha5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Arruda