swarm_sdk 2.7.14 → 3.0.0.alpha1
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_sdk/ruby_llm_patches/chat_callbacks_patch.rb +16 -0
- data/lib/swarm_sdk/ruby_llm_patches/init.rb +4 -1
- data/lib/swarm_sdk/v3/agent.rb +1165 -0
- data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
- data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
- data/lib/swarm_sdk/v3/configuration.rb +490 -0
- data/lib/swarm_sdk/v3/debug_log.rb +86 -0
- data/lib/swarm_sdk/v3/event_stream.rb +130 -0
- data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
- data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
- data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
- data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
- data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
- data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
- data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
- data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
- data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
- data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
- data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
- data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
- data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
- data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
- data/lib/swarm_sdk/v3/memory/card.rb +206 -0
- data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
- data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
- data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
- data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
- data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
- data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
- data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
- data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
- data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
- data/lib/swarm_sdk/v3/memory/store.rb +489 -0
- data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
- data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
- data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
- data/lib/swarm_sdk/v3/tools/base.rb +80 -0
- data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
- data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
- data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
- data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
- data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
- data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
- data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
- data/lib/swarm_sdk/v3/tools/read.rb +181 -0
- data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
- data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
- data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
- data/lib/swarm_sdk/v3/tools/think.rb +88 -0
- data/lib/swarm_sdk/v3/tools/write.rb +87 -0
- data/lib/swarm_sdk/v3.rb +145 -0
- metadata +83 -148
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
- data/lib/swarm_sdk/agent/builder.rb +0 -705
- data/lib/swarm_sdk/agent/chat.rb +0 -1438
- data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
- 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 -85
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
- 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 -134
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
- data/lib/swarm_sdk/agent/context.rb +0 -115
- data/lib/swarm_sdk/agent/context_manager.rb +0 -315
- data/lib/swarm_sdk/agent/definition.rb +0 -588
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
- data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
- data/lib/swarm_sdk/agent_registry.rb +0 -146
- data/lib/swarm_sdk/builders/base_builder.rb +0 -558
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
- data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
- data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
- data/lib/swarm_sdk/concerns/validatable.rb +0 -55
- data/lib/swarm_sdk/config.rb +0 -368
- data/lib/swarm_sdk/configuration/parser.rb +0 -397
- data/lib/swarm_sdk/configuration/translator.rb +0 -285
- data/lib/swarm_sdk/configuration.rb +0 -165
- data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
- data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
- 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/custom_tool_registry.rb +0 -226
- data/lib/swarm_sdk/defaults.rb +0 -251
- 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 -256
- 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 -44002
- data/lib/swarm_sdk/models.rb +0 -161
- 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 -248
- 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 -119
- data/lib/swarm_sdk/restore_result.rb +0 -65
- data/lib/swarm_sdk/result.rb +0 -241
- 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 -648
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -204
- data/lib/swarm_sdk/swarm/builder.rb +0 -256
- data/lib/swarm_sdk/swarm/executor.rb +0 -446
- data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
- data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
- data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
- data/lib/swarm_sdk/swarm.rb +0 -973
- data/lib/swarm_sdk/swarm_loader.rb +0 -145
- data/lib/swarm_sdk/swarm_registry.rb +0 -136
- data/lib/swarm_sdk/tools/base.rb +0 -63
- data/lib/swarm_sdk/tools/bash.rb +0 -280
- data/lib/swarm_sdk/tools/clock.rb +0 -46
- data/lib/swarm_sdk/tools/delegate.rb +0 -389
- 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 -167
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
- data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
- 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 -273
- 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 -100
- data/lib/swarm_sdk/tools/todo_write.rb +0 -237
- data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
- data/lib/swarm_sdk/tools/write.rb +0 -112
- data/lib/swarm_sdk/transcript_builder.rb +0 -278
- 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 -95
- data/lib/swarm_sdk/workflow/builder.rb +0 -227
- data/lib/swarm_sdk/workflow/executor.rb +0 -497
- data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
- data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
- data/lib/swarm_sdk/workflow.rb +0 -589
- data/lib/swarm_sdk.rb +0 -721
|
@@ -1,226 +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
|
-
accumulated_raw_chunks = []
|
|
37
|
-
|
|
38
|
-
# Emit request event
|
|
39
|
-
emit_request_event(env, start_time)
|
|
40
|
-
|
|
41
|
-
# Wrap existing on_data to capture raw SSE chunks for streaming
|
|
42
|
-
if env.request&.on_data
|
|
43
|
-
original_on_data = env.request.on_data
|
|
44
|
-
env.request.on_data = proc do |chunk, bytes, response_env|
|
|
45
|
-
# Capture raw chunk BEFORE RubyLLM processes it
|
|
46
|
-
accumulated_raw_chunks << chunk
|
|
47
|
-
# Call original handler (RubyLLM's stream processing)
|
|
48
|
-
original_on_data.call(chunk, bytes, response_env)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
# Execute request
|
|
53
|
-
@app.call(env).on_complete do |response_env|
|
|
54
|
-
end_time = Time.now
|
|
55
|
-
|
|
56
|
-
# Determine if this was a streaming request based on whether chunks were accumulated
|
|
57
|
-
# This is more reliable than parsing response content
|
|
58
|
-
is_streaming = accumulated_raw_chunks.any?
|
|
59
|
-
|
|
60
|
-
# For streaming: use accumulated raw SSE chunks
|
|
61
|
-
# For non-streaming: use response body
|
|
62
|
-
raw_body = is_streaming ? accumulated_raw_chunks.join : response_env.body
|
|
63
|
-
|
|
64
|
-
# Store SSE body in Fiber-local for citation extraction
|
|
65
|
-
# This allows append_citations_to_content to access the full SSE body
|
|
66
|
-
# even though response.body is empty for streaming responses
|
|
67
|
-
Fiber[:last_sse_body] = raw_body if is_streaming
|
|
68
|
-
|
|
69
|
-
# Emit response event
|
|
70
|
-
timing = { start_time: start_time, end_time: end_time, duration: end_time - start_time }
|
|
71
|
-
emit_response_event(response_env, timing, raw_body, is_streaming)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
private
|
|
76
|
-
|
|
77
|
-
# Emit request event
|
|
78
|
-
#
|
|
79
|
-
# @param env [Faraday::Env] Request environment
|
|
80
|
-
# @param timestamp [Time] Request timestamp
|
|
81
|
-
# @return [void]
|
|
82
|
-
def emit_request_event(env, timestamp)
|
|
83
|
-
request_data = {
|
|
84
|
-
provider: @provider_name,
|
|
85
|
-
url: env.url.to_s,
|
|
86
|
-
body: parse_body(env.body),
|
|
87
|
-
timestamp: timestamp.utc.iso8601,
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
@on_request.call(request_data)
|
|
91
|
-
rescue StandardError => e
|
|
92
|
-
# Don't let logging errors break the request
|
|
93
|
-
LogStream.emit_error(e, source: "llm_instrumentation_middleware", context: "emit_request_event", provider: @provider_name)
|
|
94
|
-
RubyLLM.logger.debug("LLM instrumentation request error: #{e.message}")
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
# Emit response event
|
|
98
|
-
#
|
|
99
|
-
# @param env [Faraday::Env] Response environment
|
|
100
|
-
# @param timing [Hash] Timing information with :start_time, :end_time, :duration keys
|
|
101
|
-
# @param raw_body [String, nil] Raw response body (SSE stream for streaming, JSON for non-streaming)
|
|
102
|
-
# @param streaming [Boolean] Whether this was a streaming response (determined by chunk accumulation)
|
|
103
|
-
# @return [void]
|
|
104
|
-
def emit_response_event(env, timing, raw_body, streaming)
|
|
105
|
-
response_data = {
|
|
106
|
-
provider: @provider_name,
|
|
107
|
-
body: parse_body(raw_body),
|
|
108
|
-
streaming: streaming,
|
|
109
|
-
duration_seconds: timing[:duration].round(3),
|
|
110
|
-
timestamp: timing[:end_time].utc.iso8601,
|
|
111
|
-
status: env.status,
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
# Extract usage information from response body if available
|
|
115
|
-
if raw_body.is_a?(String) && !raw_body.empty?
|
|
116
|
-
begin
|
|
117
|
-
if streaming
|
|
118
|
-
# For streaming, parse the LAST SSE event which contains usage
|
|
119
|
-
# Skip "[DONE]" marker and find the last actual data event
|
|
120
|
-
last_data_line = raw_body.split("\n").reverse.find { |l| l.start_with?("data:") && !l.include?("[DONE]") }
|
|
121
|
-
if last_data_line
|
|
122
|
-
parsed = JSON.parse(last_data_line.sub(/^data:\s*/, ""))
|
|
123
|
-
response_data[:usage] = extract_usage(parsed) if parsed.is_a?(Hash)
|
|
124
|
-
response_data[:model] = parsed["model"] if parsed.is_a?(Hash)
|
|
125
|
-
end
|
|
126
|
-
else
|
|
127
|
-
# For non-streaming, parse the full JSON response
|
|
128
|
-
parsed = JSON.parse(raw_body)
|
|
129
|
-
response_data[:usage] = extract_usage(parsed) if parsed.is_a?(Hash)
|
|
130
|
-
response_data[:model] = parsed["model"] if parsed.is_a?(Hash)
|
|
131
|
-
response_data[:finish_reason] = extract_finish_reason(parsed) if parsed.is_a?(Hash)
|
|
132
|
-
end
|
|
133
|
-
rescue JSON::ParserError
|
|
134
|
-
# Not JSON, skip usage extraction
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
@on_response.call(response_data)
|
|
139
|
-
rescue StandardError => e
|
|
140
|
-
# Don't let logging errors break the response
|
|
141
|
-
LogStream.emit_error(e, source: "llm_instrumentation_middleware", context: "emit_response_event", provider: @provider_name)
|
|
142
|
-
RubyLLM.logger.debug("LLM instrumentation response error: #{e.message}")
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# Sanitize headers by removing sensitive data
|
|
146
|
-
#
|
|
147
|
-
# @param headers [Hash] HTTP headers
|
|
148
|
-
# @return [Hash] Sanitized headers
|
|
149
|
-
def sanitize_headers(headers)
|
|
150
|
-
return {} unless headers
|
|
151
|
-
|
|
152
|
-
headers.transform_keys(&:to_s).transform_values do |value|
|
|
153
|
-
# Redact authorization headers
|
|
154
|
-
if value.to_s.match?(/bearer|token|key/i)
|
|
155
|
-
"[REDACTED]"
|
|
156
|
-
else
|
|
157
|
-
value.to_s
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
rescue StandardError
|
|
161
|
-
{}
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# Parse request/response body
|
|
165
|
-
#
|
|
166
|
-
# For requests: returns parsed JSON hash
|
|
167
|
-
# For responses: returns full body (JSON parsed or raw string for SSE)
|
|
168
|
-
#
|
|
169
|
-
# @param body [String, Hash, nil] HTTP body
|
|
170
|
-
# @return [Hash, String, nil] Parsed body
|
|
171
|
-
def parse_body(body)
|
|
172
|
-
return if body.nil? || body == ""
|
|
173
|
-
|
|
174
|
-
# Already parsed
|
|
175
|
-
return body if body.is_a?(Hash)
|
|
176
|
-
|
|
177
|
-
# Try to parse JSON
|
|
178
|
-
JSON.parse(body)
|
|
179
|
-
rescue JSON::ParserError
|
|
180
|
-
# Return full body for SSE/non-JSON responses
|
|
181
|
-
# Don't truncate - let consumers decide how to handle large bodies
|
|
182
|
-
body.to_s
|
|
183
|
-
rescue StandardError
|
|
184
|
-
nil
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
# Extract usage statistics from response
|
|
188
|
-
#
|
|
189
|
-
# Handles different provider formats (OpenAI, Anthropic, etc.)
|
|
190
|
-
#
|
|
191
|
-
# @param parsed [Hash] Parsed response body
|
|
192
|
-
# @return [Hash, nil] Usage statistics
|
|
193
|
-
def extract_usage(parsed)
|
|
194
|
-
usage = parsed["usage"] || parsed.dig("usage")
|
|
195
|
-
return unless usage
|
|
196
|
-
|
|
197
|
-
{
|
|
198
|
-
input_tokens: usage["input_tokens"] || usage["prompt_tokens"],
|
|
199
|
-
output_tokens: usage["output_tokens"] || usage["completion_tokens"],
|
|
200
|
-
total_tokens: usage["total_tokens"],
|
|
201
|
-
}.compact
|
|
202
|
-
rescue StandardError
|
|
203
|
-
nil
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
# Extract finish reason from response
|
|
207
|
-
#
|
|
208
|
-
# Handles different provider formats
|
|
209
|
-
#
|
|
210
|
-
# @param parsed [Hash] Parsed response body
|
|
211
|
-
# @return [String, nil] Finish reason
|
|
212
|
-
def extract_finish_reason(parsed)
|
|
213
|
-
# Anthropic format
|
|
214
|
-
return parsed["stop_reason"] if parsed["stop_reason"]
|
|
215
|
-
|
|
216
|
-
# OpenAI format
|
|
217
|
-
choices = parsed["choices"]
|
|
218
|
-
return unless choices&.is_a?(Array) && !choices.empty?
|
|
219
|
-
|
|
220
|
-
choices.first["finish_reason"]
|
|
221
|
-
rescue StandardError
|
|
222
|
-
nil
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
end
|
|
226
|
-
end
|
|
@@ -1,173 +0,0 @@
|
|
|
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
|
-
# @param disable_environment_info [Boolean] Whether to omit environment info from prompt
|
|
32
|
-
# @return [String] Complete system prompt
|
|
33
|
-
def build(custom_prompt:, coding_agent:, disable_default_tools:, directory:, definition:,
|
|
34
|
-
disable_environment_info: false)
|
|
35
|
-
new(
|
|
36
|
-
custom_prompt: custom_prompt,
|
|
37
|
-
coding_agent: coding_agent,
|
|
38
|
-
disable_default_tools: disable_default_tools,
|
|
39
|
-
directory: directory,
|
|
40
|
-
definition: definition,
|
|
41
|
-
disable_environment_info: disable_environment_info,
|
|
42
|
-
).build
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def initialize(custom_prompt:, coding_agent:, disable_default_tools:, directory:, definition:,
|
|
47
|
-
disable_environment_info: false)
|
|
48
|
-
@custom_prompt = custom_prompt
|
|
49
|
-
@coding_agent = coding_agent
|
|
50
|
-
@disable_default_tools = disable_default_tools
|
|
51
|
-
@directory = directory
|
|
52
|
-
@definition = definition
|
|
53
|
-
@disable_environment_info = disable_environment_info
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def build
|
|
57
|
-
prompt = base_prompt_section
|
|
58
|
-
prompt = append_plugin_contributions(prompt)
|
|
59
|
-
prompt
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
private
|
|
63
|
-
|
|
64
|
-
def base_prompt_section
|
|
65
|
-
if @coding_agent
|
|
66
|
-
build_coding_agent_prompt
|
|
67
|
-
elsif default_tools_enabled?
|
|
68
|
-
build_non_coding_agent_prompt
|
|
69
|
-
else
|
|
70
|
-
(@custom_prompt || "").to_s
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def build_coding_agent_prompt
|
|
75
|
-
rendered_base = render_base_system_prompt
|
|
76
|
-
|
|
77
|
-
if @custom_prompt && !@custom_prompt.strip.empty?
|
|
78
|
-
"#{rendered_base}\n\n#{@custom_prompt}"
|
|
79
|
-
else
|
|
80
|
-
rendered_base
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def build_non_coding_agent_prompt
|
|
85
|
-
non_coding_base = render_non_coding_base_prompt
|
|
86
|
-
|
|
87
|
-
if @custom_prompt && !@custom_prompt.strip.empty?
|
|
88
|
-
if non_coding_base.empty?
|
|
89
|
-
@custom_prompt.to_s
|
|
90
|
-
else
|
|
91
|
-
"#{non_coding_base}\n\n#{@custom_prompt}"
|
|
92
|
-
end
|
|
93
|
-
else
|
|
94
|
-
non_coding_base
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def default_tools_enabled?
|
|
99
|
-
@disable_default_tools != true
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def render_base_system_prompt
|
|
103
|
-
disable_environment_info = @disable_environment_info
|
|
104
|
-
cwd = @directory || Dir.pwd
|
|
105
|
-
platform = RUBY_PLATFORM
|
|
106
|
-
os_version = begin
|
|
107
|
-
%x(uname -sr 2>/dev/null).strip
|
|
108
|
-
rescue
|
|
109
|
-
RUBY_PLATFORM
|
|
110
|
-
end
|
|
111
|
-
date = Time.now.strftime("%Y-%m-%d")
|
|
112
|
-
|
|
113
|
-
template_content = File.read(BASE_SYSTEM_PROMPT_PATH)
|
|
114
|
-
ERB.new(template_content).result(binding)
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def render_non_coding_base_prompt
|
|
118
|
-
return "" if @disable_environment_info
|
|
119
|
-
|
|
120
|
-
cwd = @directory || Dir.pwd
|
|
121
|
-
platform = RUBY_PLATFORM
|
|
122
|
-
os_version = begin
|
|
123
|
-
%x(uname -sr 2>/dev/null).strip
|
|
124
|
-
rescue
|
|
125
|
-
RUBY_PLATFORM
|
|
126
|
-
end
|
|
127
|
-
date = Time.now.strftime("%Y-%m-%d")
|
|
128
|
-
|
|
129
|
-
<<~PROMPT.strip
|
|
130
|
-
# Today's date
|
|
131
|
-
|
|
132
|
-
<today-date>
|
|
133
|
-
#{date}
|
|
134
|
-
#</today-date>
|
|
135
|
-
|
|
136
|
-
# Current Environment
|
|
137
|
-
|
|
138
|
-
<env>
|
|
139
|
-
Working directory: #{cwd}
|
|
140
|
-
Platform: #{platform}
|
|
141
|
-
OS Version: #{os_version}
|
|
142
|
-
</env>
|
|
143
|
-
PROMPT
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def append_plugin_contributions(prompt)
|
|
147
|
-
contributions = collect_plugin_prompt_contributions
|
|
148
|
-
return prompt if contributions.empty?
|
|
149
|
-
|
|
150
|
-
combined_contributions = contributions.join("\n\n")
|
|
151
|
-
|
|
152
|
-
if prompt && !prompt.strip.empty?
|
|
153
|
-
"#{prompt}\n\n#{combined_contributions}"
|
|
154
|
-
else
|
|
155
|
-
combined_contributions
|
|
156
|
-
end
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def collect_plugin_prompt_contributions
|
|
160
|
-
contributions = []
|
|
161
|
-
|
|
162
|
-
PluginRegistry.all.each do |plugin|
|
|
163
|
-
next unless plugin.memory_configured?(@definition)
|
|
164
|
-
|
|
165
|
-
contribution = plugin.system_prompt_contribution(agent_definition: @definition, storage: nil)
|
|
166
|
-
contributions << contribution if contribution && !contribution.strip.empty?
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
contributions
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
end
|
|
@@ -1,189 +0,0 @@
|
|
|
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
|