claude_swarm 1.0.9 → 1.0.11
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/{CHANGELOG.md → CHANGELOG.claude-swarm.md} +10 -0
- data/CLAUDE.md +346 -191
- data/decisions/2025-11-22-001-global-agent-registry.md +172 -0
- data/docs/v2/CHANGELOG.swarm_cli.md +20 -0
- data/docs/v2/CHANGELOG.swarm_memory.md +146 -1
- data/docs/v2/CHANGELOG.swarm_sdk.md +433 -10
- data/docs/v2/README.md +20 -5
- data/docs/v2/guides/complete-tutorial.md +95 -9
- data/docs/v2/guides/getting-started.md +10 -8
- data/docs/v2/guides/memory-adapters.md +41 -0
- data/docs/v2/guides/migrating-to-2.x.md +746 -0
- data/docs/v2/guides/plugins.md +52 -5
- data/docs/v2/guides/rails-integration.md +6 -0
- data/docs/v2/guides/snapshots.md +14 -14
- data/docs/v2/guides/swarm-memory.md +2 -13
- data/docs/v2/reference/architecture-flow.md +3 -3
- data/docs/v2/reference/cli.md +0 -1
- data/docs/v2/reference/configuration_reference.md +300 -0
- data/docs/v2/reference/event_payload_structures.md +27 -5
- data/docs/v2/reference/ruby-dsl.md +614 -18
- data/docs/v2/reference/swarm_memory_technical_details.md +7 -29
- data/docs/v2/reference/yaml.md +172 -54
- data/examples/snapshot_demo.rb +2 -2
- data/lib/claude_swarm/mcp_generator.rb +8 -21
- data/lib/claude_swarm/orchestrator.rb +8 -1
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/swarm_cli/commands/run.rb +2 -2
- data/lib/swarm_cli/config_loader.rb +11 -11
- data/lib/swarm_cli/formatters/human_formatter.rb +0 -33
- data/lib/swarm_cli/interactive_repl.rb +2 -2
- data/lib/swarm_cli/ui/icons.rb +0 -23
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
- data/lib/swarm_memory/core/semantic_index.rb +10 -2
- data/lib/swarm_memory/core/storage.rb +7 -2
- data/lib/swarm_memory/dsl/memory_config.rb +37 -0
- data/lib/swarm_memory/integration/sdk_plugin.rb +201 -28
- data/lib/swarm_memory/optimization/defragmenter.rb +1 -1
- data/lib/swarm_memory/prompts/memory_researcher.md.erb +0 -1
- data/lib/swarm_memory/tools/load_skill.rb +0 -1
- data/lib/swarm_memory/tools/memory_edit.rb +2 -1
- data/lib/swarm_memory/tools/memory_read.rb +1 -1
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +8 -6
- data/lib/swarm_sdk/agent/builder.rb +58 -0
- data/lib/swarm_sdk/agent/chat.rb +527 -1061
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +13 -88
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +108 -46
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +267 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +3 -3
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +11 -13
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +146 -0
- data/lib/swarm_sdk/agent/context.rb +1 -2
- data/lib/swarm_sdk/agent/definition.rb +66 -154
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
- data/lib/swarm_sdk/agent_registry.rb +146 -0
- data/lib/swarm_sdk/builders/base_builder.rb +488 -0
- data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
- data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
- data/lib/swarm_sdk/concerns/validatable.rb +55 -0
- data/lib/swarm_sdk/config.rb +302 -0
- data/lib/swarm_sdk/configuration/parser.rb +373 -0
- data/lib/swarm_sdk/configuration/translator.rb +255 -0
- data/lib/swarm_sdk/configuration.rb +77 -546
- data/lib/swarm_sdk/context_compactor/token_counter.rb +2 -6
- data/lib/swarm_sdk/context_compactor.rb +6 -11
- data/lib/swarm_sdk/context_management/builder.rb +128 -0
- data/lib/swarm_sdk/context_management/context.rb +328 -0
- data/lib/swarm_sdk/custom_tool_registry.rb +226 -0
- data/lib/swarm_sdk/defaults.rb +196 -0
- data/lib/swarm_sdk/events_to_messages.rb +18 -0
- data/lib/swarm_sdk/hooks/adapter.rb +3 -3
- data/lib/swarm_sdk/hooks/shell_executor.rb +4 -2
- data/lib/swarm_sdk/log_collector.rb +179 -29
- data/lib/swarm_sdk/log_stream.rb +29 -0
- data/lib/swarm_sdk/models.json +4333 -1
- data/lib/swarm_sdk/models.rb +43 -2
- data/lib/swarm_sdk/node_context.rb +1 -1
- data/lib/swarm_sdk/observer/builder.rb +81 -0
- data/lib/swarm_sdk/observer/config.rb +45 -0
- data/lib/swarm_sdk/observer/manager.rb +236 -0
- data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
- data/lib/swarm_sdk/plugin.rb +95 -5
- data/lib/swarm_sdk/result.rb +52 -0
- data/lib/swarm_sdk/snapshot.rb +6 -6
- data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
- data/lib/swarm_sdk/state_restorer.rb +136 -151
- data/lib/swarm_sdk/state_snapshot.rb +65 -100
- data/lib/swarm_sdk/swarm/agent_initializer.rb +181 -137
- data/lib/swarm_sdk/swarm/builder.rb +44 -578
- data/lib/swarm_sdk/swarm/executor.rb +213 -0
- data/lib/swarm_sdk/swarm/hook_triggers.rb +151 -0
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +341 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
- data/lib/swarm_sdk/swarm/tool_configurator.rb +58 -140
- data/lib/swarm_sdk/swarm.rb +203 -683
- data/lib/swarm_sdk/tools/bash.rb +14 -8
- data/lib/swarm_sdk/tools/delegate.rb +61 -43
- data/lib/swarm_sdk/tools/edit.rb +8 -13
- data/lib/swarm_sdk/tools/glob.rb +12 -4
- data/lib/swarm_sdk/tools/grep.rb +7 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
- data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
- data/lib/swarm_sdk/tools/read.rb +16 -18
- data/lib/swarm_sdk/tools/registry.rb +122 -10
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +9 -5
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
- data/lib/swarm_sdk/tools/todo_write.rb +7 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +20 -17
- data/lib/swarm_sdk/tools/write.rb +8 -13
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
- data/lib/swarm_sdk/workflow/builder.rb +192 -0
- data/lib/swarm_sdk/workflow/executor.rb +497 -0
- data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +7 -5
- data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +5 -3
- data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
- data/lib/swarm_sdk.rb +294 -108
- data/rubocop/cop/security/no_reflection_methods.rb +1 -1
- data/swarm_cli.gemspec +1 -1
- data/swarm_memory.gemspec +8 -3
- data/swarm_sdk.gemspec +6 -4
- data/team_full.yml +124 -320
- metadata +42 -14
- data/lib/swarm_memory/chat_extension.rb +0 -34
- data/lib/swarm_memory/tools/memory_multi_edit.rb +0 -281
- data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
- /data/lib/swarm_memory/{errors.rb → error.rb} +0 -0
|
@@ -30,8 +30,7 @@ module SwarmSDK
|
|
|
30
30
|
# 60% triggers automatic compression, 80%/90% are informational warnings
|
|
31
31
|
CONTEXT_WARNING_THRESHOLDS = [60, 80, 90].freeze
|
|
32
32
|
|
|
33
|
-
#
|
|
34
|
-
COMPRESSION_THRESHOLD = 60
|
|
33
|
+
# NOTE: Compression threshold now accessed via SwarmSDK.config.context_compression_threshold
|
|
35
34
|
|
|
36
35
|
attr_reader :name, :delegation_tools, :metadata, :warning_thresholds_hit, :swarm_id, :parent_swarm_id
|
|
37
36
|
|
|
@@ -18,11 +18,6 @@ module SwarmSDK
|
|
|
18
18
|
# system_prompt: "You build APIs"
|
|
19
19
|
# })
|
|
20
20
|
class Definition
|
|
21
|
-
DEFAULT_MODEL = "gpt-5"
|
|
22
|
-
DEFAULT_PROVIDER = "openai"
|
|
23
|
-
DEFAULT_TIMEOUT = 300 # 5 minutes - reasoning models can take a while
|
|
24
|
-
BASE_SYSTEM_PROMPT_PATH = File.expand_path("../prompts/base_system_prompt.md.erb", __dir__)
|
|
25
|
-
|
|
26
21
|
attr_reader :name,
|
|
27
22
|
:description,
|
|
28
23
|
:model,
|
|
@@ -44,7 +39,7 @@ module SwarmSDK
|
|
|
44
39
|
:agent_permissions,
|
|
45
40
|
:assume_model_exists,
|
|
46
41
|
:hooks,
|
|
47
|
-
:
|
|
42
|
+
:plugin_configs,
|
|
48
43
|
:shared_across_delegations
|
|
49
44
|
|
|
50
45
|
attr_accessor :bypass_permissions, :max_concurrent_tools
|
|
@@ -72,14 +67,14 @@ module SwarmSDK
|
|
|
72
67
|
end
|
|
73
68
|
|
|
74
69
|
@description = config[:description]
|
|
75
|
-
@model = config[:model] ||
|
|
76
|
-
@provider = config[:provider] ||
|
|
70
|
+
@model = config[:model] || SwarmSDK.config.default_model
|
|
71
|
+
@provider = config[:provider] || SwarmSDK.config.default_provider
|
|
77
72
|
@base_url = config[:base_url]
|
|
78
73
|
@api_version = config[:api_version]
|
|
79
74
|
@context_window = config[:context_window] # Explicit context window override
|
|
80
75
|
@parameters = config[:parameters] || {}
|
|
81
76
|
@headers = Utils.stringify_keys(config[:headers] || {})
|
|
82
|
-
@timeout = config[:timeout] ||
|
|
77
|
+
@timeout = config[:timeout] || SwarmSDK.config.agent_request_timeout
|
|
83
78
|
@bypass_permissions = config[:bypass_permissions] || false
|
|
84
79
|
@max_concurrent_tools = config[:max_concurrent_tools]
|
|
85
80
|
# Always assume model exists - SwarmSDK validates models separately using models.json
|
|
@@ -100,9 +95,9 @@ module SwarmSDK
|
|
|
100
95
|
# Parse directory first so it can be used in system prompt rendering
|
|
101
96
|
@directory = parse_directory(config[:directory])
|
|
102
97
|
|
|
103
|
-
#
|
|
104
|
-
#
|
|
105
|
-
@
|
|
98
|
+
# Extract plugin configurations (generic bucket for all plugin-specific keys)
|
|
99
|
+
# This allows plugins to store their config without SDK knowing about them
|
|
100
|
+
@plugin_configs = extract_plugin_configs(config)
|
|
106
101
|
|
|
107
102
|
# Delegation isolation mode (default: false = isolated instances per delegation)
|
|
108
103
|
@shared_across_delegations = config[:shared_across_delegations] || false
|
|
@@ -132,40 +127,20 @@ module SwarmSDK
|
|
|
132
127
|
validate!
|
|
133
128
|
end
|
|
134
129
|
|
|
135
|
-
#
|
|
130
|
+
# Get plugin-specific configuration
|
|
136
131
|
#
|
|
137
|
-
#
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
# Hash (from YAML) - check for directory key
|
|
145
|
-
if @memory.is_a?(Hash)
|
|
146
|
-
directory = @memory[:directory] || @memory["directory"]
|
|
147
|
-
return !directory.nil? && !directory.to_s.strip.empty?
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
false
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
# Parse memory configuration from Hash or MemoryConfig object
|
|
132
|
+
# Plugins store their configuration in the generic plugin_configs hash.
|
|
133
|
+
# This allows SDK to remain plugin-agnostic while plugins can store
|
|
134
|
+
# arbitrary configuration.
|
|
135
|
+
#
|
|
136
|
+
# @param plugin_name [Symbol] Plugin name (e.g., :memory)
|
|
137
|
+
# @return [Object, nil] Plugin configuration or nil if not present
|
|
154
138
|
#
|
|
155
|
-
# @
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
# If it's a MemoryConfig object (duck typing - has directory, adapter, mode methods)
|
|
161
|
-
# return as-is. This could be SwarmMemory::DSL::MemoryConfig or any compatible object.
|
|
162
|
-
return memory_config if memory_config.respond_to?(:directory) &&
|
|
163
|
-
memory_config.respond_to?(:adapter) &&
|
|
164
|
-
memory_config.respond_to?(:enabled?)
|
|
165
|
-
|
|
166
|
-
# If it's a hash (from YAML), keep it as a hash
|
|
167
|
-
# Plugin will create storage adapter based on the hash values
|
|
168
|
-
memory_config
|
|
139
|
+
# @example
|
|
140
|
+
# agent_definition.plugin_config(:memory)
|
|
141
|
+
# # => { directory: "tmp/memory", mode: :researcher }
|
|
142
|
+
def plugin_config(plugin_name)
|
|
143
|
+
@plugin_configs[plugin_name.to_sym] || @plugin_configs[plugin_name.to_s]
|
|
169
144
|
end
|
|
170
145
|
|
|
171
146
|
def to_h
|
|
@@ -285,122 +260,59 @@ module SwarmSDK
|
|
|
285
260
|
end
|
|
286
261
|
|
|
287
262
|
def build_full_system_prompt(custom_prompt)
|
|
288
|
-
#
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
end
|
|
298
|
-
elsif default_tools_enabled?
|
|
299
|
-
# Non-coding agent: optionally include TODO/Scratchpad sections if default tools available
|
|
300
|
-
non_coding_base = render_non_coding_base_prompt
|
|
301
|
-
|
|
302
|
-
if custom_prompt && !custom_prompt.strip.empty?
|
|
303
|
-
# Prepend TODO/Scratchpad info before custom prompt
|
|
304
|
-
"#{non_coding_base}\n\n#{custom_prompt}"
|
|
305
|
-
else
|
|
306
|
-
# No custom prompt: just return TODO/Scratchpad info
|
|
307
|
-
non_coding_base
|
|
308
|
-
end
|
|
309
|
-
else
|
|
310
|
-
# No default tools: return only custom prompt
|
|
311
|
-
(custom_prompt || "").to_s
|
|
312
|
-
end
|
|
313
|
-
|
|
314
|
-
# Append plugin contributions to system prompt
|
|
315
|
-
plugin_contributions = collect_plugin_prompt_contributions
|
|
316
|
-
if plugin_contributions.any?
|
|
317
|
-
combined_contributions = plugin_contributions.join("\n\n")
|
|
318
|
-
prompt = if prompt && !prompt.strip.empty?
|
|
319
|
-
"#{prompt}\n\n#{combined_contributions}"
|
|
320
|
-
else
|
|
321
|
-
combined_contributions
|
|
322
|
-
end
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
prompt
|
|
326
|
-
end
|
|
327
|
-
|
|
328
|
-
# Check if default tools are enabled (i.e., not disabled)
|
|
329
|
-
#
|
|
330
|
-
# @return [Boolean] True if default tools should be included
|
|
331
|
-
def default_tools_enabled?
|
|
332
|
-
@disable_default_tools != true
|
|
263
|
+
# Delegate to SystemPromptBuilder for all prompt construction logic
|
|
264
|
+
# This keeps Definition focused on data storage while extracting complex logic
|
|
265
|
+
SystemPromptBuilder.build(
|
|
266
|
+
custom_prompt: custom_prompt,
|
|
267
|
+
coding_agent: @coding_agent,
|
|
268
|
+
disable_default_tools: @disable_default_tools,
|
|
269
|
+
directory: @directory,
|
|
270
|
+
definition: self,
|
|
271
|
+
)
|
|
333
272
|
end
|
|
334
273
|
|
|
335
|
-
def
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
os_version = begin
|
|
339
|
-
%x(uname -sr 2>/dev/null).strip
|
|
340
|
-
rescue
|
|
341
|
-
RUBY_PLATFORM
|
|
342
|
-
end
|
|
343
|
-
date = Time.now.strftime("%Y-%m-%d")
|
|
344
|
-
|
|
345
|
-
template_content = File.read(BASE_SYSTEM_PROMPT_PATH)
|
|
346
|
-
ERB.new(template_content).result(binding)
|
|
274
|
+
def parse_directory(directory_config)
|
|
275
|
+
directory_config ||= "."
|
|
276
|
+
File.expand_path(directory_config.to_s)
|
|
347
277
|
end
|
|
348
278
|
|
|
349
|
-
#
|
|
279
|
+
# Extract plugin-specific configuration keys from the config hash
|
|
350
280
|
#
|
|
351
|
-
#
|
|
352
|
-
#
|
|
281
|
+
# Standard SDK keys are filtered out, leaving only plugin-specific keys.
|
|
282
|
+
# This allows plugins to add their own configuration without SDK modifications.
|
|
353
283
|
#
|
|
354
|
-
# @
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
<today-date>
|
|
388
|
-
#{date}
|
|
389
|
-
#</today-date>
|
|
390
|
-
|
|
391
|
-
# Current Environment
|
|
392
|
-
|
|
393
|
-
<env>
|
|
394
|
-
Working directory: #{cwd}
|
|
395
|
-
Platform: #{platform}
|
|
396
|
-
OS Version: #{os_version}
|
|
397
|
-
</env>
|
|
398
|
-
PROMPT
|
|
399
|
-
end
|
|
400
|
-
|
|
401
|
-
def parse_directory(directory_config)
|
|
402
|
-
directory_config ||= "."
|
|
403
|
-
File.expand_path(directory_config.to_s)
|
|
284
|
+
# @param config [Hash] Full agent configuration
|
|
285
|
+
# @return [Hash] Plugin-specific configuration (keys not recognized by SDK)
|
|
286
|
+
def extract_plugin_configs(config)
|
|
287
|
+
standard_keys = [
|
|
288
|
+
:name,
|
|
289
|
+
:description,
|
|
290
|
+
:model,
|
|
291
|
+
:provider,
|
|
292
|
+
:base_url,
|
|
293
|
+
:api_version,
|
|
294
|
+
:context_window,
|
|
295
|
+
:parameters,
|
|
296
|
+
:headers,
|
|
297
|
+
:timeout,
|
|
298
|
+
:bypass_permissions,
|
|
299
|
+
:max_concurrent_tools,
|
|
300
|
+
:assume_model_exists,
|
|
301
|
+
:disable_default_tools,
|
|
302
|
+
:coding_agent,
|
|
303
|
+
:directory,
|
|
304
|
+
:system_prompt,
|
|
305
|
+
:tools,
|
|
306
|
+
:delegates_to,
|
|
307
|
+
:mcp_servers,
|
|
308
|
+
:hooks,
|
|
309
|
+
:default_permissions,
|
|
310
|
+
:permissions,
|
|
311
|
+
:shared_across_delegations,
|
|
312
|
+
:directories,
|
|
313
|
+
]
|
|
314
|
+
|
|
315
|
+
config.reject { |k, _| standard_keys.include?(k.to_sym) }
|
|
404
316
|
end
|
|
405
317
|
|
|
406
318
|
# Parse tools configuration with permissions support
|
|
@@ -64,7 +64,8 @@ module SwarmSDK
|
|
|
64
64
|
@on_request.call(request_data)
|
|
65
65
|
rescue StandardError => e
|
|
66
66
|
# Don't let logging errors break the request
|
|
67
|
-
|
|
67
|
+
LogStream.emit_error(e, source: "llm_instrumentation_middleware", context: "emit_request_event", provider: @provider_name)
|
|
68
|
+
RubyLLM.logger.debug("LLM instrumentation request error: #{e.message}")
|
|
68
69
|
end
|
|
69
70
|
|
|
70
71
|
# Emit response event
|
|
@@ -97,7 +98,8 @@ module SwarmSDK
|
|
|
97
98
|
@on_response.call(response_data)
|
|
98
99
|
rescue StandardError => e
|
|
99
100
|
# Don't let logging errors break the response
|
|
100
|
-
|
|
101
|
+
LogStream.emit_error(e, source: "llm_instrumentation_middleware", context: "emit_response_event", provider: @provider_name)
|
|
102
|
+
RubyLLM.logger.debug("LLM instrumentation response error: #{e.message}")
|
|
101
103
|
end
|
|
102
104
|
|
|
103
105
|
# Sanitize headers by removing sensitive data
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Agent
|
|
5
|
+
# Builds system prompts for agents
|
|
6
|
+
#
|
|
7
|
+
# This class encapsulates all system prompt construction logic, including:
|
|
8
|
+
# - Base system prompt rendering (for coding agents)
|
|
9
|
+
# - Non-coding base prompt rendering
|
|
10
|
+
# - Plugin prompt contribution collection
|
|
11
|
+
# - Combining base and custom prompts
|
|
12
|
+
#
|
|
13
|
+
# ## Safety Note for SwarmMemory Integration
|
|
14
|
+
#
|
|
15
|
+
# This is an INTERNAL helper that receives Definition attributes as input.
|
|
16
|
+
# Definition remains the single source of truth with all instance variables.
|
|
17
|
+
# SwarmMemory uses `agent_definition.instance_eval { binding }` for ERB templating,
|
|
18
|
+
# which requires all properties to be on Definition object. This helper is safe
|
|
19
|
+
# because it doesn't affect Definition's structure - it only extracts logic.
|
|
20
|
+
class SystemPromptBuilder
|
|
21
|
+
BASE_SYSTEM_PROMPT_PATH = File.expand_path("../prompts/base_system_prompt.md.erb", __dir__)
|
|
22
|
+
|
|
23
|
+
class << self
|
|
24
|
+
# Build the complete system prompt for an agent
|
|
25
|
+
#
|
|
26
|
+
# @param custom_prompt [String, nil] Custom system prompt from configuration
|
|
27
|
+
# @param coding_agent [Boolean] Whether agent is configured for coding tasks
|
|
28
|
+
# @param disable_default_tools [Boolean, Array, nil] Default tools disable configuration
|
|
29
|
+
# @param directory [String] Agent's working directory
|
|
30
|
+
# @param definition [Definition] Full definition for plugin contributions
|
|
31
|
+
# @return [String] Complete system prompt
|
|
32
|
+
def build(custom_prompt:, coding_agent:, disable_default_tools:, directory:, definition:)
|
|
33
|
+
new(
|
|
34
|
+
custom_prompt: custom_prompt,
|
|
35
|
+
coding_agent: coding_agent,
|
|
36
|
+
disable_default_tools: disable_default_tools,
|
|
37
|
+
directory: directory,
|
|
38
|
+
definition: definition,
|
|
39
|
+
).build
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def initialize(custom_prompt:, coding_agent:, disable_default_tools:, directory:, definition:)
|
|
44
|
+
@custom_prompt = custom_prompt
|
|
45
|
+
@coding_agent = coding_agent
|
|
46
|
+
@disable_default_tools = disable_default_tools
|
|
47
|
+
@directory = directory
|
|
48
|
+
@definition = definition
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def build
|
|
52
|
+
prompt = base_prompt_section
|
|
53
|
+
prompt = append_plugin_contributions(prompt)
|
|
54
|
+
prompt
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def base_prompt_section
|
|
60
|
+
if @coding_agent
|
|
61
|
+
build_coding_agent_prompt
|
|
62
|
+
elsif default_tools_enabled?
|
|
63
|
+
build_non_coding_agent_prompt
|
|
64
|
+
else
|
|
65
|
+
(@custom_prompt || "").to_s
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build_coding_agent_prompt
|
|
70
|
+
rendered_base = render_base_system_prompt
|
|
71
|
+
|
|
72
|
+
if @custom_prompt && !@custom_prompt.strip.empty?
|
|
73
|
+
"#{rendered_base}\n\n#{@custom_prompt}"
|
|
74
|
+
else
|
|
75
|
+
rendered_base
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_non_coding_agent_prompt
|
|
80
|
+
non_coding_base = render_non_coding_base_prompt
|
|
81
|
+
|
|
82
|
+
if @custom_prompt && !@custom_prompt.strip.empty?
|
|
83
|
+
"#{non_coding_base}\n\n#{@custom_prompt}"
|
|
84
|
+
else
|
|
85
|
+
non_coding_base
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def default_tools_enabled?
|
|
90
|
+
@disable_default_tools != true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def render_base_system_prompt
|
|
94
|
+
cwd = @directory || Dir.pwd
|
|
95
|
+
platform = RUBY_PLATFORM
|
|
96
|
+
os_version = begin
|
|
97
|
+
%x(uname -sr 2>/dev/null).strip
|
|
98
|
+
rescue
|
|
99
|
+
RUBY_PLATFORM
|
|
100
|
+
end
|
|
101
|
+
date = Time.now.strftime("%Y-%m-%d")
|
|
102
|
+
|
|
103
|
+
template_content = File.read(BASE_SYSTEM_PROMPT_PATH)
|
|
104
|
+
ERB.new(template_content).result(binding)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def render_non_coding_base_prompt
|
|
108
|
+
cwd = @directory || Dir.pwd
|
|
109
|
+
platform = RUBY_PLATFORM
|
|
110
|
+
os_version = begin
|
|
111
|
+
%x(uname -sr 2>/dev/null).strip
|
|
112
|
+
rescue
|
|
113
|
+
RUBY_PLATFORM
|
|
114
|
+
end
|
|
115
|
+
date = Time.now.strftime("%Y-%m-%d")
|
|
116
|
+
|
|
117
|
+
<<~PROMPT.strip
|
|
118
|
+
# Today's date
|
|
119
|
+
|
|
120
|
+
<today-date>
|
|
121
|
+
#{date}
|
|
122
|
+
#</today-date>
|
|
123
|
+
|
|
124
|
+
# Current Environment
|
|
125
|
+
|
|
126
|
+
<env>
|
|
127
|
+
Working directory: #{cwd}
|
|
128
|
+
Platform: #{platform}
|
|
129
|
+
OS Version: #{os_version}
|
|
130
|
+
</env>
|
|
131
|
+
PROMPT
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def append_plugin_contributions(prompt)
|
|
135
|
+
contributions = collect_plugin_prompt_contributions
|
|
136
|
+
return prompt if contributions.empty?
|
|
137
|
+
|
|
138
|
+
combined_contributions = contributions.join("\n\n")
|
|
139
|
+
|
|
140
|
+
if prompt && !prompt.strip.empty?
|
|
141
|
+
"#{prompt}\n\n#{combined_contributions}"
|
|
142
|
+
else
|
|
143
|
+
combined_contributions
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def collect_plugin_prompt_contributions
|
|
148
|
+
contributions = []
|
|
149
|
+
|
|
150
|
+
PluginRegistry.all.each do |plugin|
|
|
151
|
+
next unless plugin.memory_configured?(@definition)
|
|
152
|
+
|
|
153
|
+
contribution = plugin.system_prompt_contribution(agent_definition: @definition, storage: nil)
|
|
154
|
+
contributions << contribution if contribution && !contribution.strip.empty?
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
contributions
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
# Global registry for reusable agent definitions
|
|
5
|
+
#
|
|
6
|
+
# AgentRegistry allows declaring agents in separate files that can be
|
|
7
|
+
# referenced by name in swarm definitions. This promotes code reuse and
|
|
8
|
+
# separation of concerns - agent definitions can live in dedicated files
|
|
9
|
+
# while swarm configurations compose them together.
|
|
10
|
+
#
|
|
11
|
+
# ## Usage
|
|
12
|
+
#
|
|
13
|
+
# Register agents globally (typically in separate files):
|
|
14
|
+
#
|
|
15
|
+
# # agents/backend.rb
|
|
16
|
+
# SwarmSDK.agent :backend do
|
|
17
|
+
# model "claude-sonnet-4"
|
|
18
|
+
# description "Backend API developer"
|
|
19
|
+
# system_prompt "You build REST APIs"
|
|
20
|
+
# tools :Read, :Edit, :Bash
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# Reference registered agents in swarm definitions:
|
|
24
|
+
#
|
|
25
|
+
# # swarm.rb
|
|
26
|
+
# SwarmSDK.build do
|
|
27
|
+
# name "Dev Team"
|
|
28
|
+
# lead :backend
|
|
29
|
+
#
|
|
30
|
+
# agent :backend # Pulls from registry
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
# ## Override Support
|
|
34
|
+
#
|
|
35
|
+
# Registered agents can be extended with additional configuration:
|
|
36
|
+
#
|
|
37
|
+
# SwarmSDK.build do
|
|
38
|
+
# name "Dev Team"
|
|
39
|
+
# lead :backend
|
|
40
|
+
#
|
|
41
|
+
# agent :backend do
|
|
42
|
+
# # Registry config is applied first, then this block
|
|
43
|
+
# tools :CustomTool # Adds to tools from registry
|
|
44
|
+
# delegates_to :database
|
|
45
|
+
# end
|
|
46
|
+
# end
|
|
47
|
+
#
|
|
48
|
+
# @note This registry is not thread-safe. In multi-threaded environments,
|
|
49
|
+
# register all agents before spawning threads, or synchronize access
|
|
50
|
+
# externally. For typical fiber-based async usage (the default in SwarmSDK),
|
|
51
|
+
# this is not a concern.
|
|
52
|
+
#
|
|
53
|
+
class AgentRegistry
|
|
54
|
+
@agents = {}
|
|
55
|
+
|
|
56
|
+
class << self
|
|
57
|
+
# Register an agent definition block
|
|
58
|
+
#
|
|
59
|
+
# Stores a configuration block that will be executed when the agent
|
|
60
|
+
# is referenced in a swarm definition. The block receives an
|
|
61
|
+
# Agent::Builder context and can use all builder DSL methods.
|
|
62
|
+
#
|
|
63
|
+
# @param name [Symbol, String] Agent name (will be symbolized)
|
|
64
|
+
# @yield Agent configuration block using Agent::Builder DSL
|
|
65
|
+
# @return [void]
|
|
66
|
+
# @raise [ArgumentError] If no block is provided
|
|
67
|
+
# @raise [ArgumentError] If agent with same name is already registered
|
|
68
|
+
#
|
|
69
|
+
# @example Register a backend agent
|
|
70
|
+
# SwarmSDK::AgentRegistry.register(:backend) do
|
|
71
|
+
# model "claude-sonnet-4"
|
|
72
|
+
# description "Backend developer"
|
|
73
|
+
# tools :Read, :Edit, :Bash
|
|
74
|
+
# end
|
|
75
|
+
#
|
|
76
|
+
# @example Register with MCP servers
|
|
77
|
+
# SwarmSDK::AgentRegistry.register(:filesystem_agent) do
|
|
78
|
+
# model "gpt-4"
|
|
79
|
+
# description "File manager"
|
|
80
|
+
# mcp_server :fs, type: :stdio, command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem"]
|
|
81
|
+
# end
|
|
82
|
+
def register(name, &block)
|
|
83
|
+
raise ArgumentError, "Block required for agent registration" unless block_given?
|
|
84
|
+
|
|
85
|
+
sym_name = name.to_sym
|
|
86
|
+
if @agents.key?(sym_name)
|
|
87
|
+
raise ArgumentError,
|
|
88
|
+
"Agent '#{sym_name}' is already registered. " \
|
|
89
|
+
"Use SwarmSDK.clear_agent_registry! to reset, or choose a different name."
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
@agents[sym_name] = block
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Retrieve a registered agent block
|
|
96
|
+
#
|
|
97
|
+
# @param name [Symbol, String] Agent name
|
|
98
|
+
# @return [Proc, nil] The registration block or nil if not found
|
|
99
|
+
#
|
|
100
|
+
# @example
|
|
101
|
+
# block = SwarmSDK::AgentRegistry.get(:backend)
|
|
102
|
+
# builder.instance_eval(&block) if block
|
|
103
|
+
def get(name)
|
|
104
|
+
@agents[name.to_sym]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Check if an agent is registered
|
|
108
|
+
#
|
|
109
|
+
# @param name [Symbol, String] Agent name
|
|
110
|
+
# @return [Boolean] true if agent is registered
|
|
111
|
+
#
|
|
112
|
+
# @example
|
|
113
|
+
# if SwarmSDK::AgentRegistry.registered?(:backend)
|
|
114
|
+
# puts "Backend agent is available"
|
|
115
|
+
# end
|
|
116
|
+
def registered?(name)
|
|
117
|
+
@agents.key?(name.to_sym)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# List all registered agent names
|
|
121
|
+
#
|
|
122
|
+
# @return [Array<Symbol>] Names of all registered agents
|
|
123
|
+
#
|
|
124
|
+
# @example
|
|
125
|
+
# SwarmSDK::AgentRegistry.names
|
|
126
|
+
# # => [:backend, :frontend, :database]
|
|
127
|
+
def names
|
|
128
|
+
@agents.keys
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Clear all registrations
|
|
132
|
+
#
|
|
133
|
+
# Primarily useful for testing to ensure clean state between tests.
|
|
134
|
+
#
|
|
135
|
+
# @return [void]
|
|
136
|
+
#
|
|
137
|
+
# @example In test setup/teardown
|
|
138
|
+
# def teardown
|
|
139
|
+
# SwarmSDK::AgentRegistry.clear
|
|
140
|
+
# end
|
|
141
|
+
def clear
|
|
142
|
+
@agents.clear
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|