swarm_sdk 2.5.1 → 2.5.3
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/agent/RETRY_LOGIC.md +77 -29
- data/lib/swarm_sdk/agent/chat.rb +280 -33
- data/lib/swarm_sdk/agent/definition.rb +16 -1
- data/lib/swarm_sdk/models.json +4315 -4210
- data/lib/swarm_sdk/result.rb +37 -0
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +1 -0
- data/lib/swarm_sdk/transcript_builder.rb +278 -0
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk.rb +2 -3
- metadata +5 -4
data/lib/swarm_sdk/result.rb
CHANGED
|
@@ -109,6 +109,43 @@ module SwarmSDK
|
|
|
109
109
|
@logs.map { |entry| entry[:agent] }.compact.uniq.map(&:to_sym)
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
+
# Generate an LLM-readable transcript of the conversation
|
|
113
|
+
#
|
|
114
|
+
# Converts the execution logs into a human/LLM-readable format suitable for
|
|
115
|
+
# reflection, analysis, memory creation, or passing to another agent.
|
|
116
|
+
#
|
|
117
|
+
# @param agents [Array<Symbol>] Optional list of agents to filter by.
|
|
118
|
+
# If no agents specified, includes all agents.
|
|
119
|
+
# If one or more agents specified, only includes events from those agents.
|
|
120
|
+
# @param include_tool_results [Boolean] Include tool execution results (default: true)
|
|
121
|
+
# @param include_thinking [Boolean] Include agent_step content/thinking (default: false)
|
|
122
|
+
# @return [String] Formatted transcript ready for LLM consumption
|
|
123
|
+
#
|
|
124
|
+
# @example Get full transcript
|
|
125
|
+
# result.transcript
|
|
126
|
+
# # => "USER: Help me with CORS\n\nAGENT [assistant]: ..."
|
|
127
|
+
#
|
|
128
|
+
# @example Filter to specific agents
|
|
129
|
+
# result.transcript(:backend, :database)
|
|
130
|
+
# # => Only events from backend and database agents
|
|
131
|
+
#
|
|
132
|
+
# @example Single agent transcript
|
|
133
|
+
# result.transcript(:backend)
|
|
134
|
+
# # => Only events from backend agent
|
|
135
|
+
#
|
|
136
|
+
# @example Include thinking steps
|
|
137
|
+
# result.transcript(include_thinking: true)
|
|
138
|
+
# # => Includes agent_step intermediate reasoning
|
|
139
|
+
def transcript(*agents, include_tool_results: true, include_thinking: false)
|
|
140
|
+
agent_filter = agents.empty? ? nil : agents
|
|
141
|
+
TranscriptBuilder.build(
|
|
142
|
+
@logs,
|
|
143
|
+
agents: agent_filter,
|
|
144
|
+
include_tool_results: include_tool_results,
|
|
145
|
+
include_thinking: include_thinking,
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
|
|
112
149
|
# Get per-agent usage breakdown from logs
|
|
113
150
|
#
|
|
114
151
|
# Aggregates context usage, tokens, and cost for each agent from their
|
|
@@ -222,6 +222,7 @@ module SwarmSDK
|
|
|
222
222
|
agent: context.agent_name,
|
|
223
223
|
swarm_id: @swarm_id,
|
|
224
224
|
parent_swarm_id: @parent_swarm_id,
|
|
225
|
+
prompt: context.metadata[:prompt],
|
|
225
226
|
model: context.metadata[:model] || "unknown",
|
|
226
227
|
provider: context.metadata[:provider] || "unknown",
|
|
227
228
|
message_count: context.metadata[:message_count] || 0,
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
# Transforms raw log events into LLM-readable conversation transcripts
|
|
5
|
+
#
|
|
6
|
+
# TranscriptBuilder converts the structured event log from swarm execution
|
|
7
|
+
# into a human/LLM-readable format suitable for reflection, analysis, or
|
|
8
|
+
# memory creation.
|
|
9
|
+
#
|
|
10
|
+
# @example Basic usage
|
|
11
|
+
# transcript = TranscriptBuilder.build(result.logs)
|
|
12
|
+
# # => "USER: Help me with CORS\n\nAGENT [assistant]: ..."
|
|
13
|
+
#
|
|
14
|
+
# @example Filter by agents
|
|
15
|
+
# transcript = TranscriptBuilder.build(result.logs, agents: [:backend, :database])
|
|
16
|
+
#
|
|
17
|
+
# @example Custom options
|
|
18
|
+
# transcript = TranscriptBuilder.build(
|
|
19
|
+
# result.logs,
|
|
20
|
+
# include_tool_results: true,
|
|
21
|
+
# max_result_length: 1000,
|
|
22
|
+
# include_thinking: false
|
|
23
|
+
# )
|
|
24
|
+
class TranscriptBuilder
|
|
25
|
+
# Default maximum length for tool result content
|
|
26
|
+
DEFAULT_MAX_RESULT_LENGTH = 500
|
|
27
|
+
|
|
28
|
+
# Default maximum length for tool arguments
|
|
29
|
+
DEFAULT_MAX_ARGS_LENGTH = 200
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
# Build a transcript from log events
|
|
33
|
+
#
|
|
34
|
+
# @param logs [Array<Hash>] Array of log events from Result#logs
|
|
35
|
+
# @param agents [Array<Symbol>, nil] Filter to specific agents (nil = all)
|
|
36
|
+
# @param include_tool_results [Boolean] Include tool execution results (default: true)
|
|
37
|
+
# @param include_thinking [Boolean] Include agent_step content/thinking (default: false)
|
|
38
|
+
# @param max_result_length [Integer] Maximum characters for tool results
|
|
39
|
+
# @param max_args_length [Integer] Maximum characters for tool arguments
|
|
40
|
+
# @return [String] Formatted transcript
|
|
41
|
+
def build(logs, agents: nil, include_tool_results: true, include_thinking: false,
|
|
42
|
+
max_result_length: DEFAULT_MAX_RESULT_LENGTH, max_args_length: DEFAULT_MAX_ARGS_LENGTH)
|
|
43
|
+
new(
|
|
44
|
+
logs,
|
|
45
|
+
agents: agents,
|
|
46
|
+
include_tool_results: include_tool_results,
|
|
47
|
+
include_thinking: include_thinking,
|
|
48
|
+
max_result_length: max_result_length,
|
|
49
|
+
max_args_length: max_args_length,
|
|
50
|
+
).build
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Initialize a new TranscriptBuilder
|
|
55
|
+
#
|
|
56
|
+
# @param logs [Array<Hash>] Array of log events
|
|
57
|
+
# @param agents [Array<Symbol>, nil] Filter to specific agents
|
|
58
|
+
# @param include_tool_results [Boolean] Include tool execution results
|
|
59
|
+
# @param include_thinking [Boolean] Include agent_step content
|
|
60
|
+
# @param max_result_length [Integer] Maximum characters for tool results
|
|
61
|
+
# @param max_args_length [Integer] Maximum characters for tool arguments
|
|
62
|
+
def initialize(logs, agents: nil, include_tool_results: true, include_thinking: false,
|
|
63
|
+
max_result_length: DEFAULT_MAX_RESULT_LENGTH, max_args_length: DEFAULT_MAX_ARGS_LENGTH)
|
|
64
|
+
@logs = logs || []
|
|
65
|
+
@agents = normalize_agents(agents)
|
|
66
|
+
@include_tool_results = include_tool_results
|
|
67
|
+
@include_thinking = include_thinking
|
|
68
|
+
@max_result_length = max_result_length
|
|
69
|
+
@max_args_length = max_args_length
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Build the transcript
|
|
73
|
+
#
|
|
74
|
+
# @return [String] Formatted transcript
|
|
75
|
+
def build
|
|
76
|
+
@logs
|
|
77
|
+
.filter_map { |event| format_event(event) }
|
|
78
|
+
.join("\n\n")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
# Normalize agent filter to array of symbols
|
|
84
|
+
#
|
|
85
|
+
# @param agents [Array, Symbol, String, nil] Agent filter input
|
|
86
|
+
# @return [Array<Symbol>, nil] Normalized agent list or nil for all
|
|
87
|
+
def normalize_agents(agents)
|
|
88
|
+
return if agents.nil? || (agents.is_a?(Array) && agents.empty?)
|
|
89
|
+
|
|
90
|
+
Array(agents).map(&:to_sym)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Check if event passes agent filter
|
|
94
|
+
#
|
|
95
|
+
# @param event [Hash] Log event
|
|
96
|
+
# @return [Boolean] True if event should be included
|
|
97
|
+
def passes_agent_filter?(event)
|
|
98
|
+
return true if @agents.nil?
|
|
99
|
+
|
|
100
|
+
agent = event[:agent] || event["agent"]
|
|
101
|
+
return true if agent.nil? # Include events without agent (like swarm_start)
|
|
102
|
+
|
|
103
|
+
@agents.include?(agent.to_sym)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Format a single event into transcript text
|
|
107
|
+
#
|
|
108
|
+
# @param event [Hash] Log event
|
|
109
|
+
# @return [String, nil] Formatted text or nil to skip
|
|
110
|
+
def format_event(event)
|
|
111
|
+
return unless passes_agent_filter?(event)
|
|
112
|
+
|
|
113
|
+
type = event[:type] || event["type"]
|
|
114
|
+
|
|
115
|
+
case type
|
|
116
|
+
when "user_prompt"
|
|
117
|
+
format_user_prompt(event)
|
|
118
|
+
when "agent_step"
|
|
119
|
+
format_agent_step(event)
|
|
120
|
+
when "agent_stop"
|
|
121
|
+
format_agent_stop(event)
|
|
122
|
+
when "tool_call"
|
|
123
|
+
format_tool_call(event)
|
|
124
|
+
when "tool_result"
|
|
125
|
+
format_tool_result(event)
|
|
126
|
+
when "pre_delegation", "delegation_start"
|
|
127
|
+
format_delegation_start(event)
|
|
128
|
+
when "post_delegation", "delegation_complete"
|
|
129
|
+
format_delegation_complete(event)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Format user_prompt event
|
|
134
|
+
#
|
|
135
|
+
# @param event [Hash] Event data
|
|
136
|
+
# @return [String, nil] Formatted text
|
|
137
|
+
def format_user_prompt(event)
|
|
138
|
+
prompt = event[:prompt] || event["prompt"]
|
|
139
|
+
return if prompt.nil? || prompt.to_s.strip.empty?
|
|
140
|
+
|
|
141
|
+
agent = event[:agent] || event["agent"]
|
|
142
|
+
source = event[:source] || event["source"] || "user"
|
|
143
|
+
|
|
144
|
+
# Show source if it's a delegation or system message
|
|
145
|
+
prefix = case source.to_s
|
|
146
|
+
when "delegation"
|
|
147
|
+
"DELEGATION REQUEST"
|
|
148
|
+
when "system"
|
|
149
|
+
"SYSTEM"
|
|
150
|
+
else
|
|
151
|
+
"USER"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
if agent && source.to_s != "user"
|
|
155
|
+
"#{prefix} → [#{agent}]: #{prompt}"
|
|
156
|
+
else
|
|
157
|
+
"#{prefix}: #{prompt}"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Format agent_step event (intermediate response with tool calls)
|
|
162
|
+
#
|
|
163
|
+
# @param event [Hash] Event data
|
|
164
|
+
# @return [String, nil] Formatted text
|
|
165
|
+
def format_agent_step(event)
|
|
166
|
+
return unless @include_thinking
|
|
167
|
+
|
|
168
|
+
content = event[:content] || event["content"]
|
|
169
|
+
return if content.nil? || content.to_s.strip.empty?
|
|
170
|
+
|
|
171
|
+
agent = event[:agent] || event["agent"]
|
|
172
|
+
"AGENT [#{agent}] (thinking): #{content}"
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Format agent_stop event (final response)
|
|
176
|
+
#
|
|
177
|
+
# @param event [Hash] Event data
|
|
178
|
+
# @return [String, nil] Formatted text
|
|
179
|
+
def format_agent_stop(event)
|
|
180
|
+
content = event[:content] || event["content"]
|
|
181
|
+
return if content.nil? || content.to_s.strip.empty?
|
|
182
|
+
|
|
183
|
+
agent = event[:agent] || event["agent"]
|
|
184
|
+
"AGENT [#{agent}]: #{content}"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Format tool_call event
|
|
188
|
+
#
|
|
189
|
+
# @param event [Hash] Event data
|
|
190
|
+
# @return [String] Formatted text
|
|
191
|
+
def format_tool_call(event)
|
|
192
|
+
tool = event[:tool] || event["tool"] || event[:tool_name] || event["tool_name"]
|
|
193
|
+
agent = event[:agent] || event["agent"]
|
|
194
|
+
arguments = event[:arguments] || event["arguments"]
|
|
195
|
+
|
|
196
|
+
args_str = format_arguments(arguments)
|
|
197
|
+
"TOOL [#{agent}] → #{tool}(#{args_str})"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Format tool_result event
|
|
201
|
+
#
|
|
202
|
+
# @param event [Hash] Event data
|
|
203
|
+
# @return [String, nil] Formatted text
|
|
204
|
+
def format_tool_result(event)
|
|
205
|
+
return unless @include_tool_results
|
|
206
|
+
|
|
207
|
+
tool = event[:tool] || event["tool"] || event[:tool_name] || event["tool_name"]
|
|
208
|
+
result = event[:result] || event["result"]
|
|
209
|
+
# Use key existence check since false || nil would lose the false value
|
|
210
|
+
success = event.key?(:success) ? event[:success] : event["success"]
|
|
211
|
+
|
|
212
|
+
# Handle RubyLLM::ToolResult objects
|
|
213
|
+
result_content = if result.respond_to?(:content)
|
|
214
|
+
result.content
|
|
215
|
+
else
|
|
216
|
+
result.to_s
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
truncated = truncate(result_content, @max_result_length)
|
|
220
|
+
|
|
221
|
+
status = success == false ? " [FAILED]" : ""
|
|
222
|
+
"RESULT [#{tool}]#{status}: #{truncated}"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Format delegation_start event
|
|
226
|
+
#
|
|
227
|
+
# @param event [Hash] Event data
|
|
228
|
+
# @return [String, nil] Formatted text
|
|
229
|
+
def format_delegation_start(event)
|
|
230
|
+
from = event[:from_agent] || event["from_agent"] || event[:agent] || event["agent"]
|
|
231
|
+
to = event[:to_agent] || event["to_agent"]
|
|
232
|
+
task = event[:task] || event["task"] || event[:message] || event["message"]
|
|
233
|
+
|
|
234
|
+
return unless to
|
|
235
|
+
|
|
236
|
+
task_preview = truncate(task.to_s, 200)
|
|
237
|
+
"DELEGATE: #{from} → #{to}: #{task_preview}"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Format delegation_complete event
|
|
241
|
+
#
|
|
242
|
+
# @param event [Hash] Event data
|
|
243
|
+
# @return [String, nil] Formatted text
|
|
244
|
+
def format_delegation_complete(event)
|
|
245
|
+
from = event[:from_agent] || event["from_agent"] || event[:agent] || event["agent"]
|
|
246
|
+
to = event[:to_agent] || event["to_agent"]
|
|
247
|
+
|
|
248
|
+
return unless to
|
|
249
|
+
|
|
250
|
+
"DELEGATE COMPLETE: #{to} → #{from}"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Format tool arguments for display
|
|
254
|
+
#
|
|
255
|
+
# @param arguments [Hash, String, nil] Tool arguments
|
|
256
|
+
# @return [String] Formatted arguments
|
|
257
|
+
def format_arguments(arguments)
|
|
258
|
+
return "{}" if arguments.nil?
|
|
259
|
+
|
|
260
|
+
str = arguments.is_a?(String) ? arguments : arguments.to_json
|
|
261
|
+
truncate(str, @max_args_length)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Truncate text to maximum length with ellipsis
|
|
265
|
+
#
|
|
266
|
+
# @param text [String, nil] Text to truncate
|
|
267
|
+
# @param max [Integer] Maximum length
|
|
268
|
+
# @return [String] Truncated text
|
|
269
|
+
def truncate(text, max)
|
|
270
|
+
return "" if text.nil?
|
|
271
|
+
|
|
272
|
+
str = text.to_s
|
|
273
|
+
return str if str.length <= max
|
|
274
|
+
|
|
275
|
+
"#{str[0...max]}..."
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
data/lib/swarm_sdk/version.rb
CHANGED
data/lib/swarm_sdk.rb
CHANGED
|
@@ -17,15 +17,14 @@ require "async/semaphore"
|
|
|
17
17
|
require "ruby_llm"
|
|
18
18
|
require "ruby_llm/mcp"
|
|
19
19
|
|
|
20
|
-
# Patch
|
|
20
|
+
# Patch ruby_llm_swarm-mcp's Zeitwerk loader to ignore railtie.rb when Rails is not present
|
|
21
21
|
# This prevents NameError when eager loading outside of Rails applications
|
|
22
|
-
# Can be removed once https://github.com/parruda/ruby_llm-mcp/issues/XXX is fixed
|
|
23
22
|
unless defined?(Rails)
|
|
24
23
|
require "zeitwerk"
|
|
25
24
|
mcp_loader = nil
|
|
26
25
|
Zeitwerk::Registry.loaders.each { |l| mcp_loader = l if l.tag == "RubyLLM-mcp" }
|
|
27
26
|
if mcp_loader
|
|
28
|
-
mcp_gem_dir = Gem.loaded_specs["
|
|
27
|
+
mcp_gem_dir = Gem.loaded_specs["ruby_llm_swarm-mcp"]&.gem_dir
|
|
29
28
|
if mcp_gem_dir
|
|
30
29
|
railtie_path = File.join(mcp_gem_dir, "lib", "ruby_llm", "mcp", "railtie.rb")
|
|
31
30
|
mcp_loader.ignore(railtie_path)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: swarm_sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.5.
|
|
4
|
+
version: 2.5.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Paulo Arruda
|
|
@@ -234,6 +234,7 @@ files:
|
|
|
234
234
|
- lib/swarm_sdk/tools/todo_write.rb
|
|
235
235
|
- lib/swarm_sdk/tools/web_fetch.rb
|
|
236
236
|
- lib/swarm_sdk/tools/write.rb
|
|
237
|
+
- lib/swarm_sdk/transcript_builder.rb
|
|
237
238
|
- lib/swarm_sdk/utils.rb
|
|
238
239
|
- lib/swarm_sdk/validation_result.rb
|
|
239
240
|
- lib/swarm_sdk/version.rb
|
|
@@ -243,12 +244,12 @@ files:
|
|
|
243
244
|
- lib/swarm_sdk/workflow/executor.rb
|
|
244
245
|
- lib/swarm_sdk/workflow/node_builder.rb
|
|
245
246
|
- lib/swarm_sdk/workflow/transformer_executor.rb
|
|
246
|
-
homepage: https://github.com/parruda/
|
|
247
|
+
homepage: https://github.com/parruda/swarm
|
|
247
248
|
licenses:
|
|
248
249
|
- MIT
|
|
249
250
|
metadata:
|
|
250
|
-
source_code_uri: https://github.com/parruda/
|
|
251
|
-
changelog_uri: https://github.com/parruda/
|
|
251
|
+
source_code_uri: https://github.com/parruda/swarm
|
|
252
|
+
changelog_uri: https://github.com/parruda/swarm/blob/main/docs/v2/CHANGELOG.swarm_sdk.md
|
|
252
253
|
rdoc_options: []
|
|
253
254
|
require_paths:
|
|
254
255
|
- lib
|