claude_swarm 1.0.6 → 1.0.7
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/.ruby-version +1 -1
- data/CHANGELOG.md +14 -0
- data/README.md +336 -1037
- data/docs/V1_TO_V2_MIGRATION_GUIDE.md +1120 -0
- data/docs/v1/README.md +1195 -0
- data/docs/v2/CHANGELOG.swarm_cli.md +22 -0
- data/docs/v2/CHANGELOG.swarm_memory.md +20 -0
- data/docs/v2/CHANGELOG.swarm_sdk.md +287 -10
- data/docs/v2/README.md +32 -6
- data/docs/v2/guides/complete-tutorial.md +133 -37
- data/docs/v2/guides/composable-swarms.md +1178 -0
- data/docs/v2/guides/getting-started.md +42 -1
- data/docs/v2/guides/snapshots.md +1498 -0
- data/docs/v2/reference/architecture-flow.md +5 -3
- data/docs/v2/reference/event_payload_structures.md +249 -12
- data/docs/v2/reference/execution-flow.md +1 -1
- data/docs/v2/reference/ruby-dsl.md +368 -22
- data/docs/v2/reference/yaml.md +314 -63
- data/examples/snapshot_demo.rb +119 -0
- data/examples/v2/dsl/01_basic.rb +0 -2
- data/examples/v2/dsl/02_core_parameters.rb +0 -2
- data/examples/v2/dsl/03_capabilities.rb +0 -2
- data/examples/v2/dsl/04_llm_parameters.rb +0 -2
- data/examples/v2/dsl/05_advanced_flags.rb +0 -3
- data/examples/v2/dsl/06_permissions.rb +0 -4
- data/examples/v2/dsl/07_mcp_server.rb +0 -2
- data/examples/v2/dsl/08_swarm_hooks.rb +0 -2
- data/examples/v2/dsl/09_agent_hooks.rb +0 -2
- data/examples/v2/dsl/10_all_agents_hooks.rb +0 -3
- data/examples/v2/dsl/11_delegation.rb +0 -2
- data/examples/v2/dsl/12_complete_integration.rb +2 -6
- data/examples/v2/node_context_demo.rb +1 -1
- data/examples/v2/node_workflow.rb +2 -4
- data/examples/v2/plan_and_execute.rb +157 -0
- data/lib/claude_swarm/configuration.rb +28 -4
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/swarm_cli/formatters/human_formatter.rb +103 -0
- data/lib/swarm_cli/interactive_repl.rb +9 -3
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
- data/lib/swarm_memory/integration/cli_registration.rb +3 -2
- data/lib/swarm_memory/integration/sdk_plugin.rb +11 -5
- data/lib/swarm_memory/tools/memory_edit.rb +2 -2
- data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
- data/lib/swarm_memory/tools/memory_read.rb +3 -3
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +5 -0
- data/lib/swarm_sdk/agent/builder.rb +33 -0
- data/lib/swarm_sdk/agent/chat/context_tracker.rb +33 -0
- data/lib/swarm_sdk/agent/chat/hook_integration.rb +49 -3
- data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +11 -27
- data/lib/swarm_sdk/agent/chat.rb +200 -51
- data/lib/swarm_sdk/agent/context.rb +6 -2
- data/lib/swarm_sdk/agent/context_manager.rb +6 -0
- data/lib/swarm_sdk/agent/definition.rb +14 -2
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +180 -0
- data/lib/swarm_sdk/configuration.rb +387 -94
- data/lib/swarm_sdk/events_to_messages.rb +181 -0
- data/lib/swarm_sdk/log_collector.rb +31 -5
- data/lib/swarm_sdk/log_stream.rb +37 -8
- data/lib/swarm_sdk/model_aliases.json +4 -1
- data/lib/swarm_sdk/node/agent_config.rb +33 -8
- data/lib/swarm_sdk/node/builder.rb +39 -18
- data/lib/swarm_sdk/node_orchestrator.rb +293 -26
- data/lib/swarm_sdk/proc_helpers.rb +53 -0
- data/lib/swarm_sdk/providers/openai_with_responses.rb +22 -15
- data/lib/swarm_sdk/restore_result.rb +65 -0
- data/lib/swarm_sdk/snapshot.rb +156 -0
- data/lib/swarm_sdk/snapshot_from_events.rb +386 -0
- data/lib/swarm_sdk/state_restorer.rb +491 -0
- data/lib/swarm_sdk/state_snapshot.rb +369 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +360 -55
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
- data/lib/swarm_sdk/swarm/builder.rb +208 -12
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +46 -11
- data/lib/swarm_sdk/swarm.rb +338 -42
- data/lib/swarm_sdk/swarm_loader.rb +145 -0
- data/lib/swarm_sdk/swarm_registry.rb +136 -0
- data/lib/swarm_sdk/tools/delegate.rb +92 -7
- data/lib/swarm_sdk/tools/read.rb +17 -5
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +45 -0
- data/lib/swarm_sdk/utils.rb +18 -0
- data/lib/swarm_sdk/validation_result.rb +33 -0
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk.rb +40 -8
- data/swarm_cli.gemspec +1 -1
- data/swarm_memory.gemspec +2 -2
- data/swarm_sdk.gemspec +2 -2
- metadata +21 -13
- data/examples/learning-assistant/assistant.md +0 -7
- data/examples/learning-assistant/example-memories/concept-example.md +0 -90
- data/examples/learning-assistant/example-memories/experience-example.md +0 -66
- data/examples/learning-assistant/example-memories/fact-example.md +0 -76
- data/examples/learning-assistant/example-memories/memory-index.md +0 -78
- data/examples/learning-assistant/example-memories/skill-example.md +0 -168
- data/examples/learning-assistant/learning_assistant.rb +0 -34
- data/examples/learning-assistant/learning_assistant.yml +0 -20
- data/lib/swarm_sdk/mcp.rb +0 -16
- data/llm.v2.txt +0 -13407
- /data/docs/v2/guides/{MEMORY_DEFRAG_GUIDE.md → memory-defrag-guide.md} +0 -0
- /data/{llms.txt → llms.claude-swarm.txt} +0 -0
|
@@ -8,10 +8,6 @@
|
|
|
8
8
|
# Run: bundle exec ruby -Ilib lib/swarm_sdk/examples/dsl/06_permissions.rb
|
|
9
9
|
|
|
10
10
|
require "swarm_sdk"
|
|
11
|
-
require_relative "../../../swarm_sdk/swarm_builder"
|
|
12
|
-
require_relative "../../../swarm_sdk/agent_builder"
|
|
13
|
-
require_relative "../../../swarm_sdk/all_agents_builder"
|
|
14
|
-
require_relative "../../../swarm_sdk/permissions_builder"
|
|
15
11
|
require "fileutils"
|
|
16
12
|
|
|
17
13
|
ENV["OPENAI_API_KEY"] = "test-key"
|
|
@@ -8,9 +8,6 @@
|
|
|
8
8
|
# Run: bundle exec ruby -Ilib lib/swarm_sdk/examples/dsl/10_all_agents_hooks.rb
|
|
9
9
|
|
|
10
10
|
require "swarm_sdk"
|
|
11
|
-
require_relative "../../../swarm_sdk/swarm_builder"
|
|
12
|
-
require_relative "../../../swarm_sdk/agent_builder"
|
|
13
|
-
require_relative "../../../swarm_sdk/all_agents_builder"
|
|
14
11
|
|
|
15
12
|
ENV["OPENAI_API_KEY"] = "test-key"
|
|
16
13
|
|
|
@@ -8,10 +8,6 @@
|
|
|
8
8
|
# Run: bundle exec ruby -Ilib lib/swarm_sdk/examples/dsl/12_complete_integration.rb
|
|
9
9
|
|
|
10
10
|
require "swarm_sdk"
|
|
11
|
-
require_relative "../../../swarm_sdk/swarm_builder"
|
|
12
|
-
require_relative "../../../swarm_sdk/agent_builder"
|
|
13
|
-
require_relative "../../../swarm_sdk/all_agents_builder"
|
|
14
|
-
require_relative "../../../swarm_sdk/permissions_builder"
|
|
15
11
|
|
|
16
12
|
ENV["OPENAI_API_KEY"] = "test-key"
|
|
17
13
|
|
|
@@ -76,7 +72,7 @@ swarm = SwarmSDK.build do
|
|
|
76
72
|
|
|
77
73
|
# Advanced flags
|
|
78
74
|
bypass_permissions(false)
|
|
79
|
-
|
|
75
|
+
coding_agent(false)
|
|
80
76
|
assume_model_exists(true)
|
|
81
77
|
timeout(120)
|
|
82
78
|
|
|
@@ -113,7 +109,7 @@ puts " ✓ Agent identity (system_prompt, description)"
|
|
|
113
109
|
puts " ✓ Capabilities (tools, delegates_to, directory)"
|
|
114
110
|
puts " ✓ MCP servers (filesystem via stdio)"
|
|
115
111
|
puts " ✓ LLM params (parameters, timeout)"
|
|
116
|
-
puts " ✓ Advanced flags (disable_default_tools, bypass_permissions,
|
|
112
|
+
puts " ✓ Advanced flags (disable_default_tools, bypass_permissions, coding_agent, assume_model_exists)"
|
|
117
113
|
puts " ✓ Permissions (all_agents and agent-level)"
|
|
118
114
|
puts " ✓ Hooks (swarm-level, agent-level, all_agents)"
|
|
119
115
|
puts " ✓ Delegation"
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
#
|
|
14
14
|
# Run: ruby examples/node_workflow.rb
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
require "swarm_sdk"
|
|
17
17
|
|
|
18
18
|
swarm = SwarmSDK.build do
|
|
19
19
|
name("Haiku Workflow")
|
|
@@ -27,7 +27,6 @@ swarm = SwarmSDK.build do
|
|
|
27
27
|
Your job is to break down tasks into smaller subtasks. Extract the intent of the user's prompt and break it down into smaller subtasks.
|
|
28
28
|
Return a list of subtasks.
|
|
29
29
|
PROMPT
|
|
30
|
-
tools(include_default: false)
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
agent(:implementer) do
|
|
@@ -37,7 +36,6 @@ swarm = SwarmSDK.build do
|
|
|
37
36
|
system_prompt(<<~PROMPT)
|
|
38
37
|
Your job is to execute the subtasks given to you.
|
|
39
38
|
PROMPT
|
|
40
|
-
tools(include_default: false)
|
|
41
39
|
end
|
|
42
40
|
|
|
43
41
|
agent(:verifier) do
|
|
@@ -47,12 +45,12 @@ swarm = SwarmSDK.build do
|
|
|
47
45
|
system_prompt(<<~PROMPT)
|
|
48
46
|
Your job is to verify work given to you and return a summary of your findings
|
|
49
47
|
PROMPT
|
|
50
|
-
tools(include_default: false)
|
|
51
48
|
end
|
|
52
49
|
|
|
53
50
|
# Stage 1: Planning
|
|
54
51
|
node(:planning) do
|
|
55
52
|
# Input transformer - ctx.content is the initial prompt
|
|
53
|
+
# Demonstrates using return for early exit with skip_execution
|
|
56
54
|
input do |ctx|
|
|
57
55
|
<<~PROMPT
|
|
58
56
|
Please break down the following prompt into smaller subtasks:
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "swarm_sdk"
|
|
5
|
+
|
|
6
|
+
# Example: Plan and Execute Pattern
|
|
7
|
+
# Two nodes, one agent - first node plans, second node executes the plan
|
|
8
|
+
|
|
9
|
+
swarm = SwarmSDK.build do
|
|
10
|
+
name("Plan and Execute")
|
|
11
|
+
|
|
12
|
+
# Define a single agent that will be used in both nodes
|
|
13
|
+
agent(:assistant) do
|
|
14
|
+
description("A versatile assistant that can plan and execute tasks")
|
|
15
|
+
provider(:openai)
|
|
16
|
+
model("gpt-5")
|
|
17
|
+
coding_agent(false)
|
|
18
|
+
|
|
19
|
+
system_prompt(<<~PROMPT)
|
|
20
|
+
You are a helpful assistant who can both plan and execute tasks.
|
|
21
|
+
|
|
22
|
+
When planning, you should:
|
|
23
|
+
- Break down the task into clear, actionable steps
|
|
24
|
+
- Identify any dependencies or prerequisites
|
|
25
|
+
- Consider potential challenges
|
|
26
|
+
|
|
27
|
+
When executing, you should:
|
|
28
|
+
- Follow the plan carefully
|
|
29
|
+
- Use available tools effectively
|
|
30
|
+
- Report on progress and completion
|
|
31
|
+
PROMPT
|
|
32
|
+
|
|
33
|
+
# Give the assistant tools for execution
|
|
34
|
+
disable_default_tools(true)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Node 1: Planning stage
|
|
38
|
+
# The agent receives the original input and creates a plan
|
|
39
|
+
node(:planning) do
|
|
40
|
+
# Use the assistant agent in planning mode (fresh context)
|
|
41
|
+
agent(:assistant)
|
|
42
|
+
|
|
43
|
+
# Transform input for planning
|
|
44
|
+
#
|
|
45
|
+
# NOTE: Input/output blocks are automatically converted to lambdas,
|
|
46
|
+
# which means you can use `return` safely for early exits!
|
|
47
|
+
#
|
|
48
|
+
# Example of using return for conditional skip:
|
|
49
|
+
# return ctx.skip_execution(content: "cached result") if cached
|
|
50
|
+
input do |ctx|
|
|
51
|
+
<<~INPUT
|
|
52
|
+
Please create a detailed plan for the following task:
|
|
53
|
+
|
|
54
|
+
#{ctx.original_prompt}
|
|
55
|
+
|
|
56
|
+
Your plan should:
|
|
57
|
+
1. Break down the task into specific steps
|
|
58
|
+
2. Identify what needs to be done first
|
|
59
|
+
3. List any tools or resources needed
|
|
60
|
+
4. Be clear and actionable
|
|
61
|
+
|
|
62
|
+
Output your plan in a structured format.
|
|
63
|
+
INPUT
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Transform output to extract key information
|
|
67
|
+
output do |ctx|
|
|
68
|
+
# Save the plan to a file for reference
|
|
69
|
+
File.write("plan.txt", ctx.content)
|
|
70
|
+
|
|
71
|
+
# Pass the plan to the next node
|
|
72
|
+
<<~OUTPUT
|
|
73
|
+
PLAN CREATED:
|
|
74
|
+
#{ctx.content}
|
|
75
|
+
|
|
76
|
+
Now execute this plan step by step.
|
|
77
|
+
OUTPUT
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Node 2: Execution stage
|
|
82
|
+
# The agent receives the plan and executes it
|
|
83
|
+
node(:implementation) do
|
|
84
|
+
# Depends on planning node
|
|
85
|
+
depends_on(:planning)
|
|
86
|
+
|
|
87
|
+
# Use the assistant agent in execution mode (fresh context)
|
|
88
|
+
agent(:assistant)
|
|
89
|
+
|
|
90
|
+
# Transform input to provide context from planning
|
|
91
|
+
input do |ctx|
|
|
92
|
+
plan = ctx.all_results[:planning].content
|
|
93
|
+
|
|
94
|
+
<<~INPUT
|
|
95
|
+
You previously created the following plan:
|
|
96
|
+
|
|
97
|
+
#{plan}
|
|
98
|
+
|
|
99
|
+
Now execute this plan. Use the available tools (Write, Edit, Bash) to complete each step.
|
|
100
|
+
Report on what you accomplished.
|
|
101
|
+
INPUT
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Output transformer for final results
|
|
105
|
+
output do |ctx|
|
|
106
|
+
<<~OUTPUT
|
|
107
|
+
EXECUTION COMPLETE
|
|
108
|
+
|
|
109
|
+
#{ctx.content}
|
|
110
|
+
|
|
111
|
+
Plan reference: #{File.exist?("plan.txt") ? "Saved in plan.txt" : "Not saved"}
|
|
112
|
+
OUTPUT
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Set the starting node
|
|
117
|
+
start_node(:planning)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Execute the swarm
|
|
121
|
+
if __FILE__ == $PROGRAM_NAME
|
|
122
|
+
# Example task
|
|
123
|
+
task = "Create a simple Ruby script that reads a CSV file and outputs a summary"
|
|
124
|
+
|
|
125
|
+
puts "Starting Plan and Execute swarm..."
|
|
126
|
+
puts "Task: #{task}"
|
|
127
|
+
puts "\n" + "=" * 80 + "\n"
|
|
128
|
+
|
|
129
|
+
result = swarm.execute(task) do |log|
|
|
130
|
+
# Optional: Log events as they happen
|
|
131
|
+
case log[:type]
|
|
132
|
+
when "node_start"
|
|
133
|
+
puts "\n🔵 Starting node: #{log[:node]}"
|
|
134
|
+
when "node_complete"
|
|
135
|
+
puts "✅ Completed node: #{log[:node]} (#{log[:duration].round(2)}s)"
|
|
136
|
+
when "tool_call"
|
|
137
|
+
puts " 🔧 Tool: #{log[:tool]}"
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
puts "\n" + "=" * 80 + "\n"
|
|
142
|
+
|
|
143
|
+
if result.success?
|
|
144
|
+
puts "✅ Swarm execution successful!\n\n"
|
|
145
|
+
puts result.content
|
|
146
|
+
puts "\n" + "-" * 80
|
|
147
|
+
puts "Stats:"
|
|
148
|
+
puts " Duration: #{result.duration.round(2)}s"
|
|
149
|
+
puts " Total cost: $#{result.total_cost.round(4)}"
|
|
150
|
+
puts " Total tokens: #{result.total_tokens}"
|
|
151
|
+
puts " Agents involved: #{result.agents_involved.join(", ")}"
|
|
152
|
+
else
|
|
153
|
+
puts "❌ Swarm execution failed!"
|
|
154
|
+
puts "Error: #{result.error.message}"
|
|
155
|
+
puts result.error.backtrace.first(5).join("\n")
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -69,19 +69,43 @@ module ClaudeSwarm
|
|
|
69
69
|
validate_directories unless has_before_commands?
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
-
def interpolate_env_vars!(obj)
|
|
72
|
+
def interpolate_env_vars!(obj, path = [])
|
|
73
73
|
case obj
|
|
74
74
|
when String
|
|
75
|
-
|
|
75
|
+
# Skip interpolation for any values inside MCP configurations
|
|
76
|
+
# Check if we're inside an mcps array element (path like: [..., "instances", <name>, "mcps", <index>, ...])
|
|
77
|
+
if in_mcp_config?(path)
|
|
78
|
+
obj
|
|
79
|
+
else
|
|
80
|
+
interpolate_env_string(obj)
|
|
81
|
+
end
|
|
76
82
|
when Hash
|
|
77
|
-
obj.
|
|
83
|
+
obj.each do |key, value|
|
|
84
|
+
obj[key] = interpolate_env_vars!(value, path + [key])
|
|
85
|
+
end
|
|
86
|
+
obj
|
|
78
87
|
when Array
|
|
79
|
-
obj.map
|
|
88
|
+
obj.map!.with_index { |v, i| interpolate_env_vars!(v, path + [i]) }
|
|
80
89
|
else
|
|
81
90
|
obj
|
|
82
91
|
end
|
|
83
92
|
end
|
|
84
93
|
|
|
94
|
+
def in_mcp_config?(path)
|
|
95
|
+
# Check if we're inside an MCP configuration
|
|
96
|
+
# Pattern: [..., "instances", instance_name, "mcps", index, ...]
|
|
97
|
+
return false if path.size < 4
|
|
98
|
+
|
|
99
|
+
# Find the position of "mcps" in the path
|
|
100
|
+
mcps_index = path.rindex("mcps")
|
|
101
|
+
return false unless mcps_index
|
|
102
|
+
|
|
103
|
+
# Check if this is under instances and followed by an array index
|
|
104
|
+
return false if mcps_index < 2
|
|
105
|
+
|
|
106
|
+
path[mcps_index - 2] == "instances" && path[mcps_index + 1].is_a?(Integer)
|
|
107
|
+
end
|
|
108
|
+
|
|
85
109
|
def interpolate_env_string(str)
|
|
86
110
|
str.gsub(ENV_VAR_WITH_DEFAULT_PATTERN) do |_match|
|
|
87
111
|
env_var = Regexp.last_match(1)
|
data/lib/claude_swarm/version.rb
CHANGED
|
@@ -95,11 +95,20 @@ module SwarmCLI
|
|
|
95
95
|
handle_breakpoint_enter(entry)
|
|
96
96
|
when "breakpoint_exit"
|
|
97
97
|
handle_breakpoint_exit(entry)
|
|
98
|
+
when "llm_retry_attempt"
|
|
99
|
+
handle_llm_retry_attempt(entry)
|
|
100
|
+
when "llm_retry_exhausted"
|
|
101
|
+
handle_llm_retry_exhausted(entry)
|
|
102
|
+
when "response_parse_error"
|
|
103
|
+
handle_response_parse_error(entry)
|
|
98
104
|
end
|
|
99
105
|
end
|
|
100
106
|
|
|
101
107
|
# Called when swarm execution completes successfully
|
|
102
108
|
def on_success(result:)
|
|
109
|
+
# Defensive: ensure all spinners are stopped before showing result
|
|
110
|
+
@spinner_manager.stop_all
|
|
111
|
+
|
|
103
112
|
if @mode == :non_interactive
|
|
104
113
|
# Full result display with summary
|
|
105
114
|
@output.puts
|
|
@@ -115,6 +124,9 @@ module SwarmCLI
|
|
|
115
124
|
|
|
116
125
|
# Called when swarm execution fails
|
|
117
126
|
def on_error(error:, duration: nil)
|
|
127
|
+
# Defensive: ensure all spinners are stopped before showing error
|
|
128
|
+
@spinner_manager.stop_all
|
|
129
|
+
|
|
118
130
|
@output.puts
|
|
119
131
|
@output.puts @divider.full
|
|
120
132
|
print_error(error)
|
|
@@ -575,6 +587,97 @@ module SwarmCLI
|
|
|
575
587
|
@output.puts
|
|
576
588
|
end
|
|
577
589
|
|
|
590
|
+
def handle_llm_retry_attempt(entry)
|
|
591
|
+
agent = entry[:agent]
|
|
592
|
+
attempt = entry[:attempt]
|
|
593
|
+
max_retries = entry[:max_retries]
|
|
594
|
+
error_class = entry[:error_class]
|
|
595
|
+
error_message = entry[:error_message]
|
|
596
|
+
retry_delay = entry[:retry_delay]
|
|
597
|
+
|
|
598
|
+
# Stop agent thinking spinner (if active)
|
|
599
|
+
unless @quiet
|
|
600
|
+
spinner_key = "agent_#{agent}".to_sym
|
|
601
|
+
@spinner_manager.stop(spinner_key) if @spinner_manager.active?(spinner_key)
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
lines = [
|
|
605
|
+
@pastel.yellow("LLM API request failed (attempt #{attempt}/#{max_retries})"),
|
|
606
|
+
@pastel.dim("Error: #{error_class}: #{error_message}"),
|
|
607
|
+
@pastel.dim("Retrying in #{retry_delay}s..."),
|
|
608
|
+
]
|
|
609
|
+
|
|
610
|
+
@output.puts @panel.render(
|
|
611
|
+
type: :warning,
|
|
612
|
+
title: "RETRY #{@agent_badge.render(agent)}",
|
|
613
|
+
lines: lines,
|
|
614
|
+
indent: @depth_tracker.get(agent),
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
# Restart spinner for next attempt
|
|
618
|
+
unless @quiet
|
|
619
|
+
spinner_key = "agent_#{agent}".to_sym
|
|
620
|
+
@spinner_manager.start(spinner_key, "#{agent} is retrying...")
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def handle_llm_retry_exhausted(entry)
|
|
625
|
+
agent = entry[:agent]
|
|
626
|
+
attempts = entry[:attempts]
|
|
627
|
+
error_class = entry[:error_class]
|
|
628
|
+
error_message = entry[:error_message]
|
|
629
|
+
|
|
630
|
+
# Stop agent thinking spinner (if active)
|
|
631
|
+
unless @quiet
|
|
632
|
+
spinner_key = "agent_#{agent}".to_sym
|
|
633
|
+
@spinner_manager.stop(spinner_key) if @spinner_manager.active?(spinner_key)
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
lines = [
|
|
637
|
+
@pastel.red("LLM API request failed after #{attempts} attempts"),
|
|
638
|
+
@pastel.dim("Error: #{error_class}: #{error_message}"),
|
|
639
|
+
@pastel.dim("No more retries available"),
|
|
640
|
+
]
|
|
641
|
+
|
|
642
|
+
@output.puts @panel.render(
|
|
643
|
+
type: :error,
|
|
644
|
+
title: "RETRY EXHAUSTED #{@agent_badge.render(agent)}",
|
|
645
|
+
lines: lines,
|
|
646
|
+
indent: @depth_tracker.get(agent),
|
|
647
|
+
)
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def handle_response_parse_error(entry)
|
|
651
|
+
agent = entry[:agent]
|
|
652
|
+
error_class = entry[:error_class]
|
|
653
|
+
error_message = entry[:error_message]
|
|
654
|
+
|
|
655
|
+
# Stop agent thinking spinner (if active)
|
|
656
|
+
unless @quiet
|
|
657
|
+
spinner_key = "agent_#{agent}".to_sym
|
|
658
|
+
@spinner_manager.stop(spinner_key) if @spinner_manager.active?(spinner_key)
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
lines = [
|
|
662
|
+
@pastel.red("Failed to parse LLM API response"),
|
|
663
|
+
@pastel.dim("Error: #{error_class}: #{error_message}"),
|
|
664
|
+
]
|
|
665
|
+
|
|
666
|
+
# Add response body preview if available (truncated)
|
|
667
|
+
if entry[:response_body]
|
|
668
|
+
body_preview = entry[:response_body].to_s[0..200]
|
|
669
|
+
body_preview += "..." if entry[:response_body].to_s.length > 200
|
|
670
|
+
lines << @pastel.dim("Response: #{body_preview}")
|
|
671
|
+
end
|
|
672
|
+
|
|
673
|
+
@output.puts @panel.render(
|
|
674
|
+
type: :error,
|
|
675
|
+
title: "PARSE ERROR #{@agent_badge.render(agent)}",
|
|
676
|
+
lines: lines,
|
|
677
|
+
indent: @depth_tracker.get(agent),
|
|
678
|
+
)
|
|
679
|
+
end
|
|
680
|
+
|
|
578
681
|
def display_todo_list(agent, timestamp)
|
|
579
682
|
todos = SwarmSDK::Tools::Stores::TodoManager.get_todos(agent.to_sym)
|
|
580
683
|
indent = @depth_tracker.indent(agent)
|
|
@@ -81,6 +81,9 @@ module SwarmCLI
|
|
|
81
81
|
display_session_summary
|
|
82
82
|
exit(130)
|
|
83
83
|
ensure
|
|
84
|
+
# Defensive: ensure all spinners are stopped on exit
|
|
85
|
+
@formatter&.spinner_manager&.stop_all
|
|
86
|
+
|
|
84
87
|
# Save history on exit
|
|
85
88
|
save_persistent_history
|
|
86
89
|
end
|
|
@@ -432,11 +435,12 @@ module SwarmCLI
|
|
|
432
435
|
end
|
|
433
436
|
end
|
|
434
437
|
|
|
438
|
+
# CRITICAL: Stop all spinners after execution completes
|
|
439
|
+
# This ensures spinner doesn't interfere with error/success display or REPL prompt
|
|
440
|
+
@formatter.spinner_manager.stop_all
|
|
441
|
+
|
|
435
442
|
# Handle cancellation (result is nil when cancelled)
|
|
436
443
|
if result.nil?
|
|
437
|
-
# Stop all active spinners
|
|
438
|
-
@formatter.spinner_manager.stop_all
|
|
439
|
-
|
|
440
444
|
puts ""
|
|
441
445
|
puts @colors[:warning].call("✗ Request cancelled by user")
|
|
442
446
|
puts ""
|
|
@@ -459,6 +463,8 @@ module SwarmCLI
|
|
|
459
463
|
# Add response to history
|
|
460
464
|
@conversation_history << { role: "agent", content: result.content }
|
|
461
465
|
rescue StandardError => e
|
|
466
|
+
# Defensive: ensure spinners are stopped on exception
|
|
467
|
+
@formatter.spinner_manager.stop_all
|
|
462
468
|
@formatter.on_error(error: e)
|
|
463
469
|
end
|
|
464
470
|
|
data/lib/swarm_cli/version.rb
CHANGED
|
@@ -2,40 +2,77 @@
|
|
|
2
2
|
|
|
3
3
|
module SwarmMemory
|
|
4
4
|
module Core
|
|
5
|
-
# StorageReadTracker manages read-entry tracking for all agents
|
|
5
|
+
# StorageReadTracker manages read-entry tracking for all agents with content digest verification
|
|
6
6
|
#
|
|
7
7
|
# This module maintains a global registry of which memory entries each agent
|
|
8
|
-
# has read during their conversation
|
|
9
|
-
# "read-before-edit" rule that ensures agents
|
|
8
|
+
# has read during their conversation along with SHA256 digests of the content.
|
|
9
|
+
# This enables enforcement of the "read-before-edit" rule that ensures agents
|
|
10
|
+
# have context before modifying entries, AND prevents editing entries that have
|
|
11
|
+
# changed externally since being read.
|
|
10
12
|
#
|
|
11
|
-
# Each agent maintains an independent
|
|
13
|
+
# Each agent maintains an independent map of read entries to content digests.
|
|
12
14
|
module StorageReadTracker
|
|
13
|
-
@read_entries = {}
|
|
15
|
+
@read_entries = {} # { agent_id => { entry_path => sha256_digest } }
|
|
14
16
|
@mutex = Mutex.new
|
|
15
17
|
|
|
16
18
|
class << self
|
|
17
|
-
# Register that an agent has read a storage entry
|
|
19
|
+
# Register that an agent has read a storage entry with content digest
|
|
18
20
|
#
|
|
19
21
|
# @param agent_id [Symbol] The agent identifier
|
|
20
22
|
# @param entry_path [String] The storage entry path
|
|
21
|
-
# @
|
|
22
|
-
|
|
23
|
+
# @param content [String] Entry content (for digest calculation)
|
|
24
|
+
# @return [String] The calculated SHA256 digest
|
|
25
|
+
def register_read(agent_id, entry_path, content)
|
|
23
26
|
@mutex.synchronize do
|
|
24
|
-
@read_entries[agent_id] ||=
|
|
25
|
-
|
|
27
|
+
@read_entries[agent_id] ||= {}
|
|
28
|
+
digest = Digest::SHA256.hexdigest(content)
|
|
29
|
+
@read_entries[agent_id][entry_path] = digest
|
|
30
|
+
digest
|
|
26
31
|
end
|
|
27
32
|
end
|
|
28
33
|
|
|
29
|
-
# Check if an agent has read
|
|
34
|
+
# Check if an agent has read an entry AND content hasn't changed
|
|
30
35
|
#
|
|
31
36
|
# @param agent_id [Symbol] The agent identifier
|
|
32
37
|
# @param entry_path [String] The storage entry path
|
|
33
|
-
# @
|
|
34
|
-
|
|
38
|
+
# @param storage [Storage] Storage instance to read current content
|
|
39
|
+
# @return [Boolean] true if agent read entry and content matches
|
|
40
|
+
def entry_read?(agent_id, entry_path, storage)
|
|
35
41
|
@mutex.synchronize do
|
|
36
42
|
return false unless @read_entries[agent_id]
|
|
37
43
|
|
|
38
|
-
@read_entries[agent_id]
|
|
44
|
+
stored_digest = @read_entries[agent_id][entry_path]
|
|
45
|
+
return false unless stored_digest
|
|
46
|
+
|
|
47
|
+
# Check if entry still matches stored digest
|
|
48
|
+
begin
|
|
49
|
+
current_content = storage.read(file_path: entry_path)
|
|
50
|
+
current_digest = Digest::SHA256.hexdigest(current_content)
|
|
51
|
+
current_digest == stored_digest
|
|
52
|
+
rescue StandardError
|
|
53
|
+
false # Entry deleted or inaccessible
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Get all read entries with digests for snapshot
|
|
59
|
+
#
|
|
60
|
+
# @param agent_id [Symbol] The agent identifier
|
|
61
|
+
# @return [Hash] { entry_path => digest }
|
|
62
|
+
def get_read_entries(agent_id)
|
|
63
|
+
@mutex.synchronize do
|
|
64
|
+
@read_entries[agent_id]&.dup || {}
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Restore read entries with digests from snapshot
|
|
69
|
+
#
|
|
70
|
+
# @param agent_id [Symbol] The agent identifier
|
|
71
|
+
# @param entries_with_digests [Hash] { entry_path => digest }
|
|
72
|
+
# @return [void]
|
|
73
|
+
def restore_read_entries(agent_id, entries_with_digests)
|
|
74
|
+
@mutex.synchronize do
|
|
75
|
+
@read_entries[agent_id] = entries_with_digests.dup
|
|
39
76
|
end
|
|
40
77
|
end
|
|
41
78
|
|
|
@@ -13,8 +13,9 @@ module SwarmMemory
|
|
|
13
13
|
#
|
|
14
14
|
# @return [void]
|
|
15
15
|
def register!
|
|
16
|
-
# Only register if SwarmCLI is
|
|
17
|
-
|
|
16
|
+
# Only register if SwarmCLI::CommandRegistry is available
|
|
17
|
+
# Check for the specific class, not just the module
|
|
18
|
+
return unless defined?(SwarmCLI::CommandRegistry)
|
|
18
19
|
|
|
19
20
|
# Load CLI commands explicitly (Zeitwerk might not have loaded it yet)
|
|
20
21
|
require_relative "../cli/commands"
|
|
@@ -250,9 +250,12 @@ module SwarmMemory
|
|
|
250
250
|
:interactive # Default
|
|
251
251
|
end
|
|
252
252
|
|
|
253
|
-
#
|
|
254
|
-
|
|
255
|
-
|
|
253
|
+
# V7.0: Extract base name for storage tracking (delegation instances share storage)
|
|
254
|
+
base_name = agent_name.to_s.split("@").first.to_sym
|
|
255
|
+
|
|
256
|
+
# Store storage and mode using BASE NAME
|
|
257
|
+
@storages[base_name] = storage # ← Changed from agent_name to base_name
|
|
258
|
+
@modes[base_name] = mode # ← Changed from agent_name to base_name
|
|
256
259
|
|
|
257
260
|
# Get mode-specific tools
|
|
258
261
|
allowed_tools = tools_for_mode(mode)
|
|
@@ -298,9 +301,12 @@ module SwarmMemory
|
|
|
298
301
|
# @param is_first_message [Boolean] True if first message
|
|
299
302
|
# @return [Array<String>] System reminders (0-2 reminders)
|
|
300
303
|
def on_user_message(agent_name:, prompt:, is_first_message:)
|
|
301
|
-
storage
|
|
304
|
+
# V7.0: Extract base name for storage lookup (delegation instances share storage)
|
|
305
|
+
base_name = agent_name.to_s.split("@").first.to_sym
|
|
306
|
+
storage = @storages[base_name] # ← Changed from agent_name to base_name
|
|
307
|
+
|
|
302
308
|
return [] unless storage&.semantic_index
|
|
303
|
-
return [] if prompt.empty?
|
|
309
|
+
return [] if prompt.nil? || prompt.empty?
|
|
304
310
|
|
|
305
311
|
# Adaptive threshold based on query length
|
|
306
312
|
# Short queries use lower threshold as they have less semantic richness
|
|
@@ -124,8 +124,8 @@ module SwarmMemory
|
|
|
124
124
|
# Read current content (this will raise ArgumentError if entry doesn't exist)
|
|
125
125
|
content = @storage.read(file_path: file_path)
|
|
126
126
|
|
|
127
|
-
# Enforce read-before-edit
|
|
128
|
-
unless Core::StorageReadTracker.entry_read?(@agent_name, file_path)
|
|
127
|
+
# Enforce read-before-edit with content verification
|
|
128
|
+
unless Core::StorageReadTracker.entry_read?(@agent_name, file_path, @storage)
|
|
129
129
|
return validation_error(
|
|
130
130
|
"Cannot edit memory entry without reading it first. " \
|
|
131
131
|
"You must use MemoryRead on 'memory://#{file_path}' before editing it. " \
|