swarm_memory 2.1.5 → 2.1.6
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_memory/version.rb +1 -1
- metadata +5 -184
- data/lib/claude_swarm/base_executor.rb +0 -133
- data/lib/claude_swarm/claude_code_executor.rb +0 -349
- data/lib/claude_swarm/claude_mcp_server.rb +0 -78
- data/lib/claude_swarm/cli.rb +0 -697
- data/lib/claude_swarm/commands/ps.rb +0 -215
- data/lib/claude_swarm/commands/show.rb +0 -139
- data/lib/claude_swarm/configuration.rb +0 -373
- data/lib/claude_swarm/hooks/session_start_hook.rb +0 -42
- data/lib/claude_swarm/json_handler.rb +0 -91
- data/lib/claude_swarm/mcp_generator.rb +0 -230
- data/lib/claude_swarm/openai/chat_completion.rb +0 -256
- data/lib/claude_swarm/openai/executor.rb +0 -256
- data/lib/claude_swarm/openai/responses.rb +0 -319
- data/lib/claude_swarm/orchestrator.rb +0 -878
- data/lib/claude_swarm/process_tracker.rb +0 -78
- data/lib/claude_swarm/session_cost_calculator.rb +0 -209
- data/lib/claude_swarm/session_path.rb +0 -42
- data/lib/claude_swarm/settings_generator.rb +0 -77
- data/lib/claude_swarm/system_utils.rb +0 -46
- data/lib/claude_swarm/templates/generation_prompt.md.erb +0 -230
- data/lib/claude_swarm/tools/reset_session_tool.rb +0 -24
- data/lib/claude_swarm/tools/session_info_tool.rb +0 -24
- data/lib/claude_swarm/tools/task_tool.rb +0 -63
- data/lib/claude_swarm/version.rb +0 -5
- data/lib/claude_swarm/worktree_manager.rb +0 -475
- data/lib/claude_swarm/yaml_loader.rb +0 -22
- data/lib/claude_swarm.rb +0 -67
- data/lib/swarm_cli/cli.rb +0 -201
- data/lib/swarm_cli/command_registry.rb +0 -61
- data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
- data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
- data/lib/swarm_cli/commands/migrate.rb +0 -55
- data/lib/swarm_cli/commands/run.rb +0 -173
- data/lib/swarm_cli/config_loader.rb +0 -98
- data/lib/swarm_cli/formatters/human_formatter.rb +0 -781
- data/lib/swarm_cli/formatters/json_formatter.rb +0 -51
- data/lib/swarm_cli/interactive_repl.rb +0 -924
- data/lib/swarm_cli/mcp_serve_options.rb +0 -44
- data/lib/swarm_cli/mcp_tools_options.rb +0 -59
- data/lib/swarm_cli/migrate_options.rb +0 -54
- data/lib/swarm_cli/migrator.rb +0 -132
- data/lib/swarm_cli/options.rb +0 -151
- data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
- data/lib/swarm_cli/ui/components/content_block.rb +0 -120
- data/lib/swarm_cli/ui/components/divider.rb +0 -57
- data/lib/swarm_cli/ui/components/panel.rb +0 -62
- data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
- data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
- data/lib/swarm_cli/ui/formatters/number.rb +0 -58
- data/lib/swarm_cli/ui/formatters/text.rb +0 -77
- data/lib/swarm_cli/ui/formatters/time.rb +0 -73
- data/lib/swarm_cli/ui/icons.rb +0 -36
- data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
- data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
- data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
- data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
- data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
- data/lib/swarm_cli/version.rb +0 -5
- data/lib/swarm_cli.rb +0 -46
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -127
- data/lib/swarm_sdk/agent/builder.rb +0 -552
- data/lib/swarm_sdk/agent/chat.rb +0 -774
- data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -268
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
- data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -78
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -233
- data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
- data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -136
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -98
- data/lib/swarm_sdk/agent/context.rb +0 -116
- data/lib/swarm_sdk/agent/context_manager.rb +0 -315
- data/lib/swarm_sdk/agent/definition.rb +0 -477
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -182
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
- data/lib/swarm_sdk/builders/base_builder.rb +0 -409
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
- data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
- data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
- data/lib/swarm_sdk/concerns/validatable.rb +0 -55
- data/lib/swarm_sdk/configuration/parser.rb +0 -353
- data/lib/swarm_sdk/configuration/translator.rb +0 -255
- data/lib/swarm_sdk/configuration.rb +0 -135
- data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
- data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -106
- data/lib/swarm_sdk/context_compactor.rb +0 -335
- data/lib/swarm_sdk/context_management/builder.rb +0 -128
- data/lib/swarm_sdk/context_management/context.rb +0 -328
- data/lib/swarm_sdk/defaults.rb +0 -196
- data/lib/swarm_sdk/events_to_messages.rb +0 -199
- data/lib/swarm_sdk/hooks/adapter.rb +0 -359
- data/lib/swarm_sdk/hooks/context.rb +0 -197
- data/lib/swarm_sdk/hooks/definition.rb +0 -80
- data/lib/swarm_sdk/hooks/error.rb +0 -29
- data/lib/swarm_sdk/hooks/executor.rb +0 -146
- data/lib/swarm_sdk/hooks/registry.rb +0 -147
- data/lib/swarm_sdk/hooks/result.rb +0 -150
- data/lib/swarm_sdk/hooks/shell_executor.rb +0 -255
- data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
- data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
- data/lib/swarm_sdk/log_collector.rb +0 -227
- data/lib/swarm_sdk/log_stream.rb +0 -127
- data/lib/swarm_sdk/markdown_parser.rb +0 -75
- data/lib/swarm_sdk/model_aliases.json +0 -8
- data/lib/swarm_sdk/models.json +0 -1
- data/lib/swarm_sdk/models.rb +0 -120
- data/lib/swarm_sdk/node_context.rb +0 -245
- data/lib/swarm_sdk/observer/builder.rb +0 -81
- data/lib/swarm_sdk/observer/config.rb +0 -45
- data/lib/swarm_sdk/observer/manager.rb +0 -236
- data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
- data/lib/swarm_sdk/permissions/config.rb +0 -239
- data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
- data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
- data/lib/swarm_sdk/permissions/validator.rb +0 -173
- data/lib/swarm_sdk/permissions_builder.rb +0 -122
- data/lib/swarm_sdk/plugin.rb +0 -309
- data/lib/swarm_sdk/plugin_registry.rb +0 -101
- data/lib/swarm_sdk/proc_helpers.rb +0 -53
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
- data/lib/swarm_sdk/restore_result.rb +0 -65
- data/lib/swarm_sdk/result.rb +0 -123
- data/lib/swarm_sdk/snapshot.rb +0 -156
- data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
- data/lib/swarm_sdk/state_restorer.rb +0 -476
- data/lib/swarm_sdk/state_snapshot.rb +0 -334
- data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -683
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -167
- data/lib/swarm_sdk/swarm/builder.rb +0 -249
- data/lib/swarm_sdk/swarm/executor.rb +0 -213
- data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -150
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -340
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -154
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
- data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -358
- data/lib/swarm_sdk/swarm.rb +0 -717
- data/lib/swarm_sdk/swarm_loader.rb +0 -145
- data/lib/swarm_sdk/swarm_registry.rb +0 -136
- data/lib/swarm_sdk/tools/bash.rb +0 -282
- data/lib/swarm_sdk/tools/clock.rb +0 -44
- data/lib/swarm_sdk/tools/delegate.rb +0 -267
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
- data/lib/swarm_sdk/tools/edit.rb +0 -145
- data/lib/swarm_sdk/tools/glob.rb +0 -166
- data/lib/swarm_sdk/tools/grep.rb +0 -235
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -163
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
- data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
- data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
- data/lib/swarm_sdk/tools/read.rb +0 -261
- data/lib/swarm_sdk/tools/registry.rb +0 -205
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -272
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
- data/lib/swarm_sdk/tools/think.rb +0 -98
- data/lib/swarm_sdk/tools/todo_write.rb +0 -235
- data/lib/swarm_sdk/tools/web_fetch.rb +0 -262
- data/lib/swarm_sdk/tools/write.rb +0 -112
- data/lib/swarm_sdk/utils.rb +0 -68
- data/lib/swarm_sdk/validation_result.rb +0 -33
- data/lib/swarm_sdk/version.rb +0 -5
- data/lib/swarm_sdk/workflow/agent_config.rb +0 -79
- data/lib/swarm_sdk/workflow/builder.rb +0 -143
- data/lib/swarm_sdk/workflow/executor.rb +0 -497
- data/lib/swarm_sdk/workflow/node_builder.rb +0 -555
- data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -249
- data/lib/swarm_sdk/workflow.rb +0 -554
- data/lib/swarm_sdk.rb +0 -524
|
@@ -1,477 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Agent
|
|
5
|
-
# Agent definition encapsulates agent configuration and builds system prompts
|
|
6
|
-
#
|
|
7
|
-
# This class is responsible for:
|
|
8
|
-
# - Parsing and validating agent configuration
|
|
9
|
-
# - Building the full system prompt (base + custom)
|
|
10
|
-
# - Handling tool permissions
|
|
11
|
-
# - Managing hooks (both DSL Ruby blocks and YAML shell commands)
|
|
12
|
-
#
|
|
13
|
-
# @example
|
|
14
|
-
# definition = Agent::Definition.new(:backend, {
|
|
15
|
-
# description: "Backend API developer",
|
|
16
|
-
# model: "gpt-5",
|
|
17
|
-
# tools: [:Read, :Write, :Bash],
|
|
18
|
-
# system_prompt: "You build APIs"
|
|
19
|
-
# })
|
|
20
|
-
class Definition
|
|
21
|
-
attr_reader :name,
|
|
22
|
-
:description,
|
|
23
|
-
:model,
|
|
24
|
-
:context_window,
|
|
25
|
-
:directory,
|
|
26
|
-
:tools,
|
|
27
|
-
:delegates_to,
|
|
28
|
-
:system_prompt,
|
|
29
|
-
:provider,
|
|
30
|
-
:base_url,
|
|
31
|
-
:api_version,
|
|
32
|
-
:mcp_servers,
|
|
33
|
-
:parameters,
|
|
34
|
-
:headers,
|
|
35
|
-
:timeout,
|
|
36
|
-
:disable_default_tools,
|
|
37
|
-
:coding_agent,
|
|
38
|
-
:default_permissions,
|
|
39
|
-
:agent_permissions,
|
|
40
|
-
:assume_model_exists,
|
|
41
|
-
:hooks,
|
|
42
|
-
:plugin_configs,
|
|
43
|
-
:shared_across_delegations
|
|
44
|
-
|
|
45
|
-
attr_accessor :bypass_permissions, :max_concurrent_tools
|
|
46
|
-
|
|
47
|
-
def initialize(name, config = {})
|
|
48
|
-
@name = name.to_sym
|
|
49
|
-
|
|
50
|
-
# Validate name doesn't contain '@' (reserved for delegation instances)
|
|
51
|
-
if @name.to_s.include?("@")
|
|
52
|
-
raise ConfigurationError,
|
|
53
|
-
"Agent names cannot contain '@' character (reserved for delegation instance naming). " \
|
|
54
|
-
"Agent: #{@name}"
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
# BREAKING CHANGE: Hard error for plural form
|
|
58
|
-
if config[:directories]
|
|
59
|
-
raise ConfigurationError,
|
|
60
|
-
"The 'directories' (plural) configuration is no longer supported in SwarmSDK 1.0+.\n\n" \
|
|
61
|
-
"Change 'directories:' to 'directory:' (singular).\n\n" \
|
|
62
|
-
"If you need access to multiple directories, use permissions:\n\n " \
|
|
63
|
-
"directory: 'backend/'\n " \
|
|
64
|
-
"permissions do\n " \
|
|
65
|
-
"tool(:Read).allow_paths('../shared/**')\n " \
|
|
66
|
-
"end"
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
@description = config[:description]
|
|
70
|
-
@model = config[:model] || Defaults::Agent::MODEL
|
|
71
|
-
@provider = config[:provider] || Defaults::Agent::PROVIDER
|
|
72
|
-
@base_url = config[:base_url]
|
|
73
|
-
@api_version = config[:api_version]
|
|
74
|
-
@context_window = config[:context_window] # Explicit context window override
|
|
75
|
-
@parameters = config[:parameters] || {}
|
|
76
|
-
@headers = Utils.stringify_keys(config[:headers] || {})
|
|
77
|
-
@timeout = config[:timeout] || Defaults::Timeouts::AGENT_REQUEST_SECONDS
|
|
78
|
-
@bypass_permissions = config[:bypass_permissions] || false
|
|
79
|
-
@max_concurrent_tools = config[:max_concurrent_tools]
|
|
80
|
-
# Always assume model exists - SwarmSDK validates models separately using models.json
|
|
81
|
-
# This prevents RubyLLM from trying to validate models in its registry
|
|
82
|
-
@assume_model_exists = true
|
|
83
|
-
|
|
84
|
-
# disable_default_tools can be:
|
|
85
|
-
# - nil/not set: include all default tools (default behavior)
|
|
86
|
-
# - true: disable ALL default tools
|
|
87
|
-
# - Array of symbols: disable specific tools (e.g., [:Think, :TodoWrite])
|
|
88
|
-
@disable_default_tools = config[:disable_default_tools]
|
|
89
|
-
|
|
90
|
-
# coding_agent defaults to false if not specified
|
|
91
|
-
# When true, includes the base system prompt for coding tasks
|
|
92
|
-
# When false, uses only the custom system prompt (no base prompt)
|
|
93
|
-
@coding_agent = config.key?(:coding_agent) ? config[:coding_agent] : false
|
|
94
|
-
|
|
95
|
-
# Parse directory first so it can be used in system prompt rendering
|
|
96
|
-
@directory = parse_directory(config[:directory])
|
|
97
|
-
|
|
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)
|
|
101
|
-
|
|
102
|
-
# Delegation isolation mode (default: false = isolated instances per delegation)
|
|
103
|
-
@shared_across_delegations = config[:shared_across_delegations] || false
|
|
104
|
-
|
|
105
|
-
# Build system prompt after directory and memory are set
|
|
106
|
-
@system_prompt = build_full_system_prompt(config[:system_prompt])
|
|
107
|
-
|
|
108
|
-
# Parse tools with permissions support
|
|
109
|
-
@default_permissions = config[:default_permissions] || {}
|
|
110
|
-
@agent_permissions = config[:permissions] || {}
|
|
111
|
-
@tools = parse_tools_with_permissions(
|
|
112
|
-
config[:tools],
|
|
113
|
-
@default_permissions,
|
|
114
|
-
@agent_permissions,
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
# Inject default write restrictions for security
|
|
118
|
-
@tools = inject_default_write_permissions(@tools)
|
|
119
|
-
|
|
120
|
-
@delegates_to = Array(config[:delegates_to] || []).map(&:to_sym).uniq
|
|
121
|
-
@mcp_servers = Array(config[:mcp_servers] || [])
|
|
122
|
-
|
|
123
|
-
# Parse hooks configuration
|
|
124
|
-
# Handles both DSL (HookDefinition objects) and YAML (raw hash) formats
|
|
125
|
-
@hooks = parse_hooks(config[:hooks])
|
|
126
|
-
|
|
127
|
-
validate!
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# Get plugin-specific configuration
|
|
131
|
-
#
|
|
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
|
|
138
|
-
#
|
|
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]
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def to_h
|
|
147
|
-
# Core SDK configuration (always serialized)
|
|
148
|
-
base_config = {
|
|
149
|
-
name: @name,
|
|
150
|
-
description: @description,
|
|
151
|
-
model: SwarmSDK::Models.resolve_alias(@model), # Resolve model aliases
|
|
152
|
-
context_window: @context_window,
|
|
153
|
-
directory: @directory,
|
|
154
|
-
tools: @tools,
|
|
155
|
-
delegates_to: @delegates_to,
|
|
156
|
-
system_prompt: @system_prompt,
|
|
157
|
-
provider: @provider,
|
|
158
|
-
base_url: @base_url,
|
|
159
|
-
api_version: @api_version,
|
|
160
|
-
mcp_servers: @mcp_servers,
|
|
161
|
-
parameters: @parameters,
|
|
162
|
-
headers: @headers,
|
|
163
|
-
timeout: @timeout,
|
|
164
|
-
bypass_permissions: @bypass_permissions,
|
|
165
|
-
disable_default_tools: @disable_default_tools,
|
|
166
|
-
coding_agent: @coding_agent,
|
|
167
|
-
assume_model_exists: @assume_model_exists,
|
|
168
|
-
max_concurrent_tools: @max_concurrent_tools,
|
|
169
|
-
hooks: @hooks,
|
|
170
|
-
shared_across_delegations: @shared_across_delegations,
|
|
171
|
-
# Permissions are core SDK functionality (not plugin-specific)
|
|
172
|
-
default_permissions: @default_permissions,
|
|
173
|
-
permissions: @agent_permissions,
|
|
174
|
-
}.compact
|
|
175
|
-
|
|
176
|
-
# Allow plugins to contribute their config for serialization
|
|
177
|
-
# This enables plugin features (memory, skills, etc.) to be preserved
|
|
178
|
-
# when cloning agents without SwarmSDK knowing about plugin-specific fields
|
|
179
|
-
plugin_configs = SwarmSDK::PluginRegistry.all.map do |plugin|
|
|
180
|
-
plugin.serialize_config(agent_definition: self)
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
# Merge plugin configs into base config
|
|
184
|
-
# Later plugins override earlier ones if they have conflicting keys
|
|
185
|
-
plugin_configs.reduce(base_config) { |acc, config| acc.merge(config) }
|
|
186
|
-
end
|
|
187
|
-
|
|
188
|
-
# Validate agent configuration and return warnings (non-fatal issues)
|
|
189
|
-
#
|
|
190
|
-
# Unlike validate! which raises exceptions for critical errors, this method
|
|
191
|
-
# returns an array of warning hashes for non-fatal issues like:
|
|
192
|
-
# - Model not found in registry (informs user, suggests alternatives)
|
|
193
|
-
# - Context tracking unavailable (useful even with assume_model_exists)
|
|
194
|
-
#
|
|
195
|
-
# Note: Validation ALWAYS runs, even with assume_model_exists: true or base_url set.
|
|
196
|
-
# The purpose is to inform the user about potential issues and suggest corrections,
|
|
197
|
-
# not to block execution.
|
|
198
|
-
#
|
|
199
|
-
# @return [Array<Hash>] Array of warning hashes
|
|
200
|
-
def validate
|
|
201
|
-
warnings = []
|
|
202
|
-
|
|
203
|
-
# Always validate model (even with assume_model_exists)
|
|
204
|
-
# Warnings inform user about typos and context tracking limitations
|
|
205
|
-
model_warning = validate_model
|
|
206
|
-
warnings << model_warning if model_warning
|
|
207
|
-
|
|
208
|
-
# Future: could add tool validation, delegate validation, etc.
|
|
209
|
-
|
|
210
|
-
warnings
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
private
|
|
214
|
-
|
|
215
|
-
# Validate that model exists in SwarmSDK's model registry
|
|
216
|
-
#
|
|
217
|
-
# Uses SwarmSDK's static models.json instead of RubyLLM's dynamic registry.
|
|
218
|
-
# This provides stable, offline model validation without network calls.
|
|
219
|
-
#
|
|
220
|
-
# Process:
|
|
221
|
-
# 1. Try to find model directly in models.json
|
|
222
|
-
# 2. If not found, try to resolve as alias and find again
|
|
223
|
-
# 3. If still not found, return warning with suggestions
|
|
224
|
-
#
|
|
225
|
-
# @return [Hash, nil] Warning hash if model not found, nil otherwise
|
|
226
|
-
def validate_model
|
|
227
|
-
# Try direct lookup first
|
|
228
|
-
model_data = SwarmSDK::Models.all.find { |m| (m["id"] || m[:id]) == @model }
|
|
229
|
-
|
|
230
|
-
# If not found, try alias resolution
|
|
231
|
-
unless model_data
|
|
232
|
-
resolved_id = SwarmSDK::Models.resolve_alias(@model)
|
|
233
|
-
# Only search again if alias was different
|
|
234
|
-
if resolved_id != @model
|
|
235
|
-
model_data = SwarmSDK::Models.all.find { |m| (m["id"] || m[:id]) == resolved_id }
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
if model_data
|
|
240
|
-
nil # Model exists (either directly or via alias)
|
|
241
|
-
else
|
|
242
|
-
# Model not found - return warning with suggestions
|
|
243
|
-
{
|
|
244
|
-
type: :model_not_found,
|
|
245
|
-
agent: @name,
|
|
246
|
-
model: @model,
|
|
247
|
-
error_message: "Unknown model: #{@model}",
|
|
248
|
-
suggestions: SwarmSDK::Models.suggest_similar(@model),
|
|
249
|
-
}
|
|
250
|
-
end
|
|
251
|
-
rescue StandardError => e
|
|
252
|
-
# Return warning on error
|
|
253
|
-
{
|
|
254
|
-
type: :model_not_found,
|
|
255
|
-
agent: @name,
|
|
256
|
-
model: @model,
|
|
257
|
-
error_message: e.message,
|
|
258
|
-
suggestions: [],
|
|
259
|
-
}
|
|
260
|
-
end
|
|
261
|
-
|
|
262
|
-
def build_full_system_prompt(custom_prompt)
|
|
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
|
-
)
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
def parse_directory(directory_config)
|
|
275
|
-
directory_config ||= "."
|
|
276
|
-
File.expand_path(directory_config.to_s)
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
# Extract plugin-specific configuration keys from the config hash
|
|
280
|
-
#
|
|
281
|
-
# Standard SDK keys are filtered out, leaving only plugin-specific keys.
|
|
282
|
-
# This allows plugins to add their own configuration without SDK modifications.
|
|
283
|
-
#
|
|
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) }
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
# Parse tools configuration with permissions support
|
|
319
|
-
#
|
|
320
|
-
# Tools can be specified as:
|
|
321
|
-
# - Symbol: :Write (no permissions)
|
|
322
|
-
# - Hash: { Write: { allowed_paths: [...] } } (with permissions)
|
|
323
|
-
#
|
|
324
|
-
# Returns array of tool configs:
|
|
325
|
-
# [
|
|
326
|
-
# { name: :Read, permissions: nil },
|
|
327
|
-
# { name: :Write, permissions: { allowed_paths: [...] } }
|
|
328
|
-
# ]
|
|
329
|
-
def parse_tools_with_permissions(tools_config, default_permissions, agent_permissions)
|
|
330
|
-
tools_array = Array(tools_config || [])
|
|
331
|
-
|
|
332
|
-
tools_array.map do |tool_spec|
|
|
333
|
-
case tool_spec
|
|
334
|
-
when Symbol, String
|
|
335
|
-
# Simple tool: :Write or "Write"
|
|
336
|
-
tool_name = tool_spec.to_sym
|
|
337
|
-
permissions = resolve_permissions(tool_name, default_permissions, agent_permissions)
|
|
338
|
-
|
|
339
|
-
{ name: tool_name, permissions: permissions }
|
|
340
|
-
when Hash
|
|
341
|
-
# Check if already in parsed format: { name: :Write, permissions: {...} }
|
|
342
|
-
if tool_spec.key?(:name)
|
|
343
|
-
# Already parsed - pass through as-is
|
|
344
|
-
tool_spec
|
|
345
|
-
else
|
|
346
|
-
# Tool with inline permissions: { Write: { allowed_paths: [...] } }
|
|
347
|
-
tool_name = tool_spec.keys.first.to_sym
|
|
348
|
-
inline_permissions = tool_spec.values.first
|
|
349
|
-
|
|
350
|
-
# Inline permissions override defaults
|
|
351
|
-
{ name: tool_name, permissions: inline_permissions }
|
|
352
|
-
end
|
|
353
|
-
else
|
|
354
|
-
raise ConfigurationError, "Invalid tool specification: #{tool_spec.inspect}"
|
|
355
|
-
end
|
|
356
|
-
end
|
|
357
|
-
end
|
|
358
|
-
|
|
359
|
-
# Resolve permissions for a tool from defaults and agent-level overrides
|
|
360
|
-
def resolve_permissions(tool_name, default_permissions, agent_permissions)
|
|
361
|
-
# Agent-level permissions override defaults
|
|
362
|
-
agent_permissions[tool_name] || default_permissions[tool_name]
|
|
363
|
-
end
|
|
364
|
-
|
|
365
|
-
# Inject default write permissions for security
|
|
366
|
-
#
|
|
367
|
-
# Write, Edit, and MultiEdit tools without explicit permissions are automatically
|
|
368
|
-
# restricted to only write within the agent's directory. This prevents accidental
|
|
369
|
-
# writes outside the agent's working scope.
|
|
370
|
-
#
|
|
371
|
-
# Default permission: { allowed_paths: ["**/*"] }
|
|
372
|
-
# This is resolved relative to the agent's directory by the permissions system.
|
|
373
|
-
#
|
|
374
|
-
# Users can override by explicitly setting permissions for these tools.
|
|
375
|
-
def inject_default_write_permissions(tools)
|
|
376
|
-
write_tools = [:Write, :Edit, :MultiEdit]
|
|
377
|
-
|
|
378
|
-
tools.map do |tool_config|
|
|
379
|
-
tool_name = tool_config[:name]
|
|
380
|
-
|
|
381
|
-
# If it's a write tool and has no permissions, inject default
|
|
382
|
-
if write_tools.include?(tool_name) && tool_config[:permissions].nil?
|
|
383
|
-
tool_config.merge(permissions: { allowed_paths: ["**/*"] })
|
|
384
|
-
else
|
|
385
|
-
tool_config
|
|
386
|
-
end
|
|
387
|
-
end
|
|
388
|
-
end
|
|
389
|
-
|
|
390
|
-
# Parse hooks configuration
|
|
391
|
-
#
|
|
392
|
-
# Handles two input formats:
|
|
393
|
-
#
|
|
394
|
-
# 1. DSL format (from Agent::Builder): Pre-parsed HookDefinition objects
|
|
395
|
-
# { event_type: [HookDefinition, ...] }
|
|
396
|
-
# These are applied directly in pass_4_configure_hooks
|
|
397
|
-
#
|
|
398
|
-
# 2. YAML format: Raw hash with shell command specifications
|
|
399
|
-
# hooks:
|
|
400
|
-
# pre_tool_use:
|
|
401
|
-
# - matcher: "Write|Edit"
|
|
402
|
-
# type: command
|
|
403
|
-
# command: "validate.sh"
|
|
404
|
-
# These are kept raw and processed by Hooks::Adapter in pass_5
|
|
405
|
-
#
|
|
406
|
-
# Returns:
|
|
407
|
-
# - DSL: { event_type: [HookDefinition, ...] }
|
|
408
|
-
# - YAML: Raw hash (for Hooks::Adapter)
|
|
409
|
-
def parse_hooks(hooks_config)
|
|
410
|
-
return {} if hooks_config.nil? || hooks_config.empty?
|
|
411
|
-
|
|
412
|
-
# If already parsed from DSL (HookDefinition objects), return as-is
|
|
413
|
-
if hooks_config.is_a?(Hash) && hooks_config.values.all? { |v| v.is_a?(Array) && v.all? { |item| item.is_a?(Hooks::Definition) } }
|
|
414
|
-
return hooks_config
|
|
415
|
-
end
|
|
416
|
-
|
|
417
|
-
# For YAML hooks: validate structure but keep raw for Hooks::Adapter
|
|
418
|
-
validate_yaml_hooks(hooks_config)
|
|
419
|
-
|
|
420
|
-
# Return raw YAML - Hooks::Adapter will process in pass_5
|
|
421
|
-
hooks_config
|
|
422
|
-
end
|
|
423
|
-
|
|
424
|
-
# Validate YAML hooks structure
|
|
425
|
-
#
|
|
426
|
-
# @param hooks_config [Hash] YAML hooks configuration
|
|
427
|
-
# @return [void]
|
|
428
|
-
def validate_yaml_hooks(hooks_config)
|
|
429
|
-
hooks_config.each do |event_name, hook_specs|
|
|
430
|
-
event_sym = event_name.to_sym
|
|
431
|
-
|
|
432
|
-
# Validate event type
|
|
433
|
-
unless Hooks::Registry::VALID_EVENTS.include?(event_sym)
|
|
434
|
-
raise ConfigurationError,
|
|
435
|
-
"Invalid hook event '#{event_name}' for agent '#{@name}'. " \
|
|
436
|
-
"Valid events: #{Hooks::Registry::VALID_EVENTS.join(", ")}"
|
|
437
|
-
end
|
|
438
|
-
|
|
439
|
-
# Validate each hook spec structure
|
|
440
|
-
Array(hook_specs).each do |spec|
|
|
441
|
-
hook_type = spec[:type] || spec["type"]
|
|
442
|
-
command = spec[:command] || spec["command"]
|
|
443
|
-
|
|
444
|
-
raise ConfigurationError, "Hook missing 'type' field for event #{event_name}" unless hook_type
|
|
445
|
-
raise ConfigurationError, "Hook missing 'command' field for event #{event_name}" if hook_type.to_s == "command" && !command
|
|
446
|
-
end
|
|
447
|
-
end
|
|
448
|
-
end
|
|
449
|
-
|
|
450
|
-
def validate!
|
|
451
|
-
raise ConfigurationError, "Agent '#{@name}' missing required 'description' field" unless @description
|
|
452
|
-
|
|
453
|
-
# Validate api_version can only be set for OpenAI-compatible providers
|
|
454
|
-
if @api_version
|
|
455
|
-
openai_compatible = ["openai", "deepseek", "perplexity", "mistral", "openrouter"]
|
|
456
|
-
unless openai_compatible.include?(@provider.to_s)
|
|
457
|
-
raise ConfigurationError,
|
|
458
|
-
"Agent '#{@name}' has api_version set, but provider is '#{@provider}'. " \
|
|
459
|
-
"api_version can only be used with OpenAI-compatible providers: #{openai_compatible.join(", ")}"
|
|
460
|
-
end
|
|
461
|
-
|
|
462
|
-
# Validate api_version value
|
|
463
|
-
valid_versions = ["v1/chat/completions", "v1/responses"]
|
|
464
|
-
unless valid_versions.include?(@api_version)
|
|
465
|
-
raise ConfigurationError,
|
|
466
|
-
"Agent '#{@name}' has invalid api_version '#{@api_version}'. " \
|
|
467
|
-
"Valid values: #{valid_versions.join(", ")}"
|
|
468
|
-
end
|
|
469
|
-
end
|
|
470
|
-
|
|
471
|
-
unless File.directory?(@directory)
|
|
472
|
-
raise ConfigurationError, "Directory '#{@directory}' for agent '#{@name}' does not exist"
|
|
473
|
-
end
|
|
474
|
-
end
|
|
475
|
-
end
|
|
476
|
-
end
|
|
477
|
-
end
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module SwarmSDK
|
|
4
|
-
module Agent
|
|
5
|
-
# Faraday middleware for capturing LLM API requests and responses
|
|
6
|
-
#
|
|
7
|
-
# This middleware intercepts HTTP calls to LLM providers and emits
|
|
8
|
-
# structured events via LogStream for logging and monitoring.
|
|
9
|
-
#
|
|
10
|
-
# Events emitted:
|
|
11
|
-
# - llm_api_request: Before sending request to LLM API
|
|
12
|
-
# - llm_api_response: After receiving response from LLM API
|
|
13
|
-
#
|
|
14
|
-
# The middleware is injected at runtime into the provider's Faraday
|
|
15
|
-
# connection stack (see Agent::Chat#inject_llm_instrumentation).
|
|
16
|
-
class LLMInstrumentationMiddleware < Faraday::Middleware
|
|
17
|
-
# Initialize middleware
|
|
18
|
-
#
|
|
19
|
-
# @param app [Faraday::Connection] Faraday app
|
|
20
|
-
# @param on_request [Proc] Callback for request events
|
|
21
|
-
# @param on_response [Proc] Callback for response events
|
|
22
|
-
# @param provider_name [String] Provider name for logging
|
|
23
|
-
def initialize(app, on_request:, on_response:, provider_name:)
|
|
24
|
-
super(app)
|
|
25
|
-
@on_request = on_request
|
|
26
|
-
@on_response = on_response
|
|
27
|
-
@provider_name = provider_name
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# Intercept HTTP call
|
|
31
|
-
#
|
|
32
|
-
# @param env [Faraday::Env] Request environment
|
|
33
|
-
# @return [Faraday::Response] HTTP response
|
|
34
|
-
def call(env)
|
|
35
|
-
start_time = Time.now
|
|
36
|
-
|
|
37
|
-
# Emit request event
|
|
38
|
-
emit_request_event(env, start_time)
|
|
39
|
-
|
|
40
|
-
# Execute request
|
|
41
|
-
@app.call(env).on_complete do |response_env|
|
|
42
|
-
end_time = Time.now
|
|
43
|
-
duration = end_time - start_time
|
|
44
|
-
|
|
45
|
-
# Emit response event
|
|
46
|
-
emit_response_event(response_env, start_time, end_time, duration)
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
# Emit request event
|
|
53
|
-
#
|
|
54
|
-
# @param env [Faraday::Env] Request environment
|
|
55
|
-
# @param timestamp [Time] Request timestamp
|
|
56
|
-
# @return [void]
|
|
57
|
-
def emit_request_event(env, timestamp)
|
|
58
|
-
request_data = {
|
|
59
|
-
provider: @provider_name,
|
|
60
|
-
body: parse_body(env.body),
|
|
61
|
-
timestamp: timestamp.utc.iso8601,
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
@on_request.call(request_data)
|
|
65
|
-
rescue StandardError => e
|
|
66
|
-
# Don't let logging errors break the request
|
|
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}")
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Emit response event
|
|
72
|
-
#
|
|
73
|
-
# @param env [Faraday::Env] Response environment
|
|
74
|
-
# @param start_time [Time] Request start time
|
|
75
|
-
# @param end_time [Time] Request end time
|
|
76
|
-
# @param duration [Float] Request duration in seconds
|
|
77
|
-
# @return [void]
|
|
78
|
-
def emit_response_event(env, start_time, end_time, duration)
|
|
79
|
-
response_data = {
|
|
80
|
-
provider: @provider_name,
|
|
81
|
-
body: parse_body(env.body),
|
|
82
|
-
duration_seconds: duration.round(3),
|
|
83
|
-
timestamp: end_time.utc.iso8601,
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
# Extract usage information from response body if available
|
|
87
|
-
if env.body.is_a?(String) && !env.body.empty?
|
|
88
|
-
begin
|
|
89
|
-
parsed = JSON.parse(env.body)
|
|
90
|
-
response_data[:usage] = extract_usage(parsed) if parsed.is_a?(Hash)
|
|
91
|
-
response_data[:model] = parsed["model"] if parsed.is_a?(Hash)
|
|
92
|
-
response_data[:finish_reason] = extract_finish_reason(parsed) if parsed.is_a?(Hash)
|
|
93
|
-
rescue JSON::ParserError
|
|
94
|
-
# Not JSON, skip usage extraction
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
@on_response.call(response_data)
|
|
99
|
-
rescue StandardError => e
|
|
100
|
-
# Don't let logging errors break the response
|
|
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}")
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Sanitize headers by removing sensitive data
|
|
106
|
-
#
|
|
107
|
-
# @param headers [Hash] HTTP headers
|
|
108
|
-
# @return [Hash] Sanitized headers
|
|
109
|
-
def sanitize_headers(headers)
|
|
110
|
-
return {} unless headers
|
|
111
|
-
|
|
112
|
-
headers.transform_keys(&:to_s).transform_values do |value|
|
|
113
|
-
# Redact authorization headers
|
|
114
|
-
if value.to_s.match?(/bearer|token|key/i)
|
|
115
|
-
"[REDACTED]"
|
|
116
|
-
else
|
|
117
|
-
value.to_s
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
rescue StandardError
|
|
121
|
-
{}
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
# Parse request/response body
|
|
125
|
-
#
|
|
126
|
-
# @param body [String, Hash, nil] HTTP body
|
|
127
|
-
# @return [Hash, String, nil] Parsed body
|
|
128
|
-
def parse_body(body)
|
|
129
|
-
return if body.nil? || body == ""
|
|
130
|
-
|
|
131
|
-
# Already parsed
|
|
132
|
-
return body if body.is_a?(Hash)
|
|
133
|
-
|
|
134
|
-
# Try to parse JSON
|
|
135
|
-
JSON.parse(body)
|
|
136
|
-
rescue JSON::ParserError
|
|
137
|
-
# Return truncated string if not JSON
|
|
138
|
-
body.to_s[0..1000]
|
|
139
|
-
rescue StandardError
|
|
140
|
-
nil
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# Extract usage statistics from response
|
|
144
|
-
#
|
|
145
|
-
# Handles different provider formats (OpenAI, Anthropic, etc.)
|
|
146
|
-
#
|
|
147
|
-
# @param parsed [Hash] Parsed response body
|
|
148
|
-
# @return [Hash, nil] Usage statistics
|
|
149
|
-
def extract_usage(parsed)
|
|
150
|
-
usage = parsed["usage"] || parsed.dig("usage")
|
|
151
|
-
return unless usage
|
|
152
|
-
|
|
153
|
-
{
|
|
154
|
-
input_tokens: usage["input_tokens"] || usage["prompt_tokens"],
|
|
155
|
-
output_tokens: usage["output_tokens"] || usage["completion_tokens"],
|
|
156
|
-
total_tokens: usage["total_tokens"],
|
|
157
|
-
}.compact
|
|
158
|
-
rescue StandardError
|
|
159
|
-
nil
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
# Extract finish reason from response
|
|
163
|
-
#
|
|
164
|
-
# Handles different provider formats
|
|
165
|
-
#
|
|
166
|
-
# @param parsed [Hash] Parsed response body
|
|
167
|
-
# @return [String, nil] Finish reason
|
|
168
|
-
def extract_finish_reason(parsed)
|
|
169
|
-
# Anthropic format
|
|
170
|
-
return parsed["stop_reason"] if parsed["stop_reason"]
|
|
171
|
-
|
|
172
|
-
# OpenAI format
|
|
173
|
-
choices = parsed["choices"]
|
|
174
|
-
return unless choices&.is_a?(Array) && !choices.empty?
|
|
175
|
-
|
|
176
|
-
choices.first["finish_reason"]
|
|
177
|
-
rescue StandardError
|
|
178
|
-
nil
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
end
|