swarm_sdk 2.6.2 → 2.7.1

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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/agent/builder.rb +33 -1
  3. data/lib/swarm_sdk/agent/chat.rb +179 -35
  4. data/lib/swarm_sdk/agent/definition.rb +7 -1
  5. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +48 -8
  6. data/lib/swarm_sdk/agent/tool_registry.rb +189 -0
  7. data/lib/swarm_sdk/builders/base_builder.rb +4 -0
  8. data/lib/swarm_sdk/config.rb +2 -1
  9. data/lib/swarm_sdk/configuration/translator.rb +2 -0
  10. data/lib/swarm_sdk/models.json +296 -238
  11. data/lib/swarm_sdk/swarm/agent_initializer.rb +51 -3
  12. data/lib/swarm_sdk/swarm/all_agents_builder.rb +9 -0
  13. data/lib/swarm_sdk/swarm/mcp_configurator.rb +45 -7
  14. data/lib/swarm_sdk/swarm/tool_configurator.rb +25 -5
  15. data/lib/swarm_sdk/tools/base.rb +63 -0
  16. data/lib/swarm_sdk/tools/bash.rb +1 -1
  17. data/lib/swarm_sdk/tools/clock.rb +3 -1
  18. data/lib/swarm_sdk/tools/delegate.rb +14 -3
  19. data/lib/swarm_sdk/tools/edit.rb +1 -1
  20. data/lib/swarm_sdk/tools/glob.rb +1 -1
  21. data/lib/swarm_sdk/tools/grep.rb +1 -1
  22. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +137 -0
  23. data/lib/swarm_sdk/tools/multi_edit.rb +1 -1
  24. data/lib/swarm_sdk/tools/read.rb +1 -1
  25. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +1 -1
  26. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +1 -1
  27. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +1 -1
  28. data/lib/swarm_sdk/tools/think.rb +3 -1
  29. data/lib/swarm_sdk/tools/todo_write.rb +3 -1
  30. data/lib/swarm_sdk/tools/web_fetch.rb +1 -1
  31. data/lib/swarm_sdk/tools/write.rb +1 -1
  32. data/lib/swarm_sdk/version.rb +1 -1
  33. metadata +4 -1
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Agent
5
+ # Per-agent tool registry managing available and active tools
6
+ #
7
+ # ## Architecture
8
+ #
9
+ # - **Available tools**: All tool instances the agent CAN use (registry)
10
+ # - **Active tools**: Subset sent to LLM based on skill state
11
+ #
12
+ # ## Thread Safety
13
+ #
14
+ # Registry access is protected by Async::Semaphore for fiber-safe operations.
15
+ #
16
+ # @example Registering tools
17
+ # registry = ToolRegistry.new
18
+ # registry.register(Read.new, source: :builtin)
19
+ # registry.register(delegate_tool, source: :delegation, metadata: { delegate_name: :backend })
20
+ #
21
+ # @example Getting active tools (no skill)
22
+ # active = registry.active_tools(skill_state: nil)
23
+ # # Returns ALL available tools
24
+ #
25
+ # @example Getting active tools (with skill)
26
+ # skill_state = SkillState.new( # From SwarmMemory plugin
27
+ # file_path: "skill/audit.md",
28
+ # tools: ["Read", "Grep"],
29
+ # permissions: { "Bash" => { deny_commands: ["rm"] } }
30
+ # )
31
+ # active = registry.active_tools(skill_state: skill_state)
32
+ # # Returns: skill's tools + non-removable tools
33
+ class ToolRegistry
34
+ # Tool metadata stored in registry
35
+ #
36
+ # @!attribute instance [r] Tool instance (possibly wrapped with permissions)
37
+ # @!attribute base_instance [r] Unwrapped tool instance (for skill permission override)
38
+ # @!attribute removable [r] Can be deactivated by skills
39
+ # @!attribute source [r] Tool source (:builtin, :delegation, :mcp, :plugin, :custom)
40
+ # @!attribute metadata [r] Source-specific metadata
41
+ ToolEntry = Data.define(:instance, :base_instance, :removable, :source, :metadata)
42
+
43
+ def initialize
44
+ @available_tools = {} # String name => ToolEntry
45
+ @mutex = Async::Semaphore.new(1) # Fiber-safe mutex
46
+ end
47
+
48
+ # Register a tool in the available tools registry
49
+ #
50
+ # @param tool [RubyLLM::Tool] Tool instance (possibly wrapped)
51
+ # @param base_tool [RubyLLM::Tool, nil] Unwrapped instance (for permission override)
52
+ # @param source [Symbol] Tool source (:builtin, :delegation, :mcp, :plugin, :custom)
53
+ # @param metadata [Hash] Source-specific metadata (server_name, plugin_name, etc.)
54
+ # @return [void]
55
+ #
56
+ # @example Register builtin tool
57
+ # registry.register(Read.new, source: :builtin)
58
+ #
59
+ # @example Register delegation tool
60
+ # registry.register(delegate_tool, source: :delegation, metadata: { delegate_name: :backend })
61
+ #
62
+ # @example Register MCP tool
63
+ # registry.register(mcp_tool, source: :mcp, metadata: { server_name: "codebase" })
64
+ #
65
+ # @example Register with permission wrapper
66
+ # wrapped_tool = PermissionWrapper.new(base_tool, permissions)
67
+ # registry.register(wrapped_tool, base_tool: base_tool, source: :builtin)
68
+ def register(tool, base_tool: nil, source:, metadata: {})
69
+ @mutex.acquire do
70
+ # Infer removability from tool class
71
+ removable = tool.respond_to?(:removable?) ? tool.removable? : true
72
+
73
+ @available_tools[tool.name] = ToolEntry.new(
74
+ instance: tool,
75
+ base_instance: base_tool || tool, # If no base, use same instance
76
+ removable: removable,
77
+ source: source,
78
+ metadata: metadata,
79
+ )
80
+ end
81
+ end
82
+
83
+ # Unregister a tool (for testing/cleanup)
84
+ #
85
+ # @param name [String, Symbol] Tool name
86
+ # @return [ToolEntry, nil] Removed entry
87
+ def unregister(name)
88
+ @mutex.acquire do
89
+ @available_tools.delete(name.to_s)
90
+ end
91
+ end
92
+
93
+ # Get active tools based on skill state
94
+ #
95
+ # Returns Hash of tool instances ready for RubyLLM::Chat.
96
+ #
97
+ # Logic:
98
+ # - If skill_state is nil: Return ALL available tools
99
+ # - If skill_state restricts tools: Return skill's tools + non-removable tools
100
+ # - Skill permissions are applied during activation (wrapping base_instance)
101
+ #
102
+ # @param skill_state [Object, nil] Skill state object (from plugin), or nil for all
103
+ # @param tool_configurator [ToolConfigurator, nil] For permission wrapping
104
+ # @param agent_definition [Agent::Definition, nil] For permission wrapping
105
+ # @return [Hash{String => RubyLLM::Tool}] name => instance mapping
106
+ #
107
+ # @example No skill loaded - all tools
108
+ # registry.active_tools(skill_state: nil)
109
+ # # => { "Read" => <Read>, "WorkWithBackend" => <Delegate>, ... }
110
+ #
111
+ # @example Skill loaded with focused toolset
112
+ # registry.active_tools(skill_state: skill_state)
113
+ # # => { "Read" => <Read>, "WorkWithBackend" => <Delegate>, "Think" => <Think>, "MemoryRead" => <MemoryRead> }
114
+ # # Includes: requested tools + non-removable tools
115
+ def active_tools(skill_state: nil, tool_configurator: nil, agent_definition: nil)
116
+ @mutex.acquire do
117
+ result = if skill_state&.restricts_tools?
118
+ # Skill loaded with tool restriction - only skill's tools + non-removable
119
+ filtered = {}
120
+
121
+ # Always include non-removable tools (use wrapped instance)
122
+ @available_tools.each do |name, entry|
123
+ filtered[name] = entry.instance unless entry.removable
124
+ end
125
+
126
+ # Add requested tools from skill
127
+ skill_state.tools.each do |name|
128
+ entry = @available_tools[name.to_s]
129
+ next unless entry
130
+
131
+ # Check if skill has custom permissions for this tool
132
+ skill_permissions = skill_state.permissions_for(name)
133
+
134
+ if skill_permissions && tool_configurator && agent_definition
135
+ # Skill overrides permissions - wrap the BASE instance
136
+ wrapped = tool_configurator.wrap_tool_with_permissions(
137
+ entry.base_instance,
138
+ skill_permissions,
139
+ agent_definition,
140
+ )
141
+ filtered[name.to_s] = wrapped
142
+ else
143
+ # No skill permission override - use registered instance
144
+ filtered[name.to_s] = entry.instance
145
+ end
146
+ end
147
+
148
+ filtered
149
+ else
150
+ # No skill OR skill doesn't restrict tools - all available tools
151
+ @available_tools.transform_values(&:instance)
152
+ end
153
+
154
+ result
155
+ end
156
+ end
157
+
158
+ # Check if tool exists in registry
159
+ #
160
+ # @param name [String, Symbol] Tool name
161
+ # @return [Boolean]
162
+ def has_tool?(name)
163
+ @available_tools.key?(name.to_s)
164
+ end
165
+
166
+ # Get all available tool names
167
+ #
168
+ # @return [Array<String>]
169
+ def tool_names
170
+ @available_tools.keys
171
+ end
172
+
173
+ # Get tool entry with metadata
174
+ #
175
+ # @param name [String, Symbol] Tool name
176
+ # @return [ToolEntry, nil]
177
+ def get(name)
178
+ @available_tools[name.to_s]
179
+ end
180
+
181
+ # Get all non-removable tool names
182
+ #
183
+ # @return [Array<String>]
184
+ def non_removable_tool_names
185
+ @available_tools.select { |_name, entry| !entry.removable }.keys
186
+ end
187
+ end
188
+ end
189
+ end
@@ -446,6 +446,10 @@ module SwarmSDK
446
446
  if !all_agents_hash[:coding_agent].nil? && !agent_builder.coding_agent_set?
447
447
  agent_builder.coding_agent(all_agents_hash[:coding_agent])
448
448
  end
449
+
450
+ if !all_agents_hash[:streaming].nil? && !agent_builder.streaming_set?
451
+ agent_builder.streaming(all_agents_hash[:streaming])
452
+ end
449
453
  end
450
454
 
451
455
  # Validate all_agents filesystem tools
@@ -94,6 +94,7 @@ module SwarmSDK
94
94
  webfetch_max_tokens: ["SWARM_SDK_WEBFETCH_MAX_TOKENS", 4096],
95
95
  allow_filesystem_tools: ["SWARM_SDK_ALLOW_FILESYSTEM_TOOLS", true],
96
96
  env_interpolation: ["SWARM_SDK_ENV_INTERPOLATION", true],
97
+ streaming: ["SWARM_SDK_STREAMING", true],
97
98
  }.freeze
98
99
 
99
100
  class << self
@@ -282,7 +283,7 @@ module SwarmSDK
282
283
  # @return [Integer, Float, Boolean, String] The parsed value
283
284
  def parse_env_value(value, key)
284
285
  case key
285
- when :allow_filesystem_tools, :env_interpolation
286
+ when :allow_filesystem_tools, :env_interpolation, :streaming
286
287
  # Convert string to boolean
287
288
  case value.to_s.downcase
288
289
  when "true", "yes", "1", "on", "enabled"
@@ -99,6 +99,7 @@ module SwarmSDK
99
99
  headers(all_agents_cfg[:headers]) if all_agents_cfg[:headers]
100
100
  coding_agent(all_agents_cfg[:coding_agent]) unless all_agents_cfg[:coding_agent].nil?
101
101
  disable_default_tools(all_agents_cfg[:disable_default_tools]) unless all_agents_cfg[:disable_default_tools].nil?
102
+ streaming(all_agents_cfg[:streaming]) unless all_agents_cfg[:streaming].nil?
102
103
 
103
104
  if all_agents_hks.any?
104
105
  all_agents_hks.each do |event, hook_specs|
@@ -162,6 +163,7 @@ module SwarmSDK
162
163
  bypass_permissions(config[:bypass_permissions]) if config[:bypass_permissions]
163
164
  disable_default_tools(config[:disable_default_tools]) unless config[:disable_default_tools].nil?
164
165
  shared_across_delegations(config[:shared_across_delegations]) unless config[:shared_across_delegations].nil?
166
+ streaming(config[:streaming]) unless config[:streaming].nil?
165
167
 
166
168
  if config[:tools]&.any?
167
169
  tool_names = config[:tools].map { |t| t.is_a?(Hash) ? t[:name] : t }