swarm_sdk 2.5.1 → 2.5.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47faf60bdab82a9c32d9e973eac8b3cc5f4130782807d8d2868417490c22cd76
4
- data.tar.gz: 120e8f3e9eec742210896b4d97dcc18c599c16ea41b70d55d54eff3e2552fec0
3
+ metadata.gz: d2d30192f340ca7aca46ae7aee4538753a28a9815ff45be7260f1ceb8415a805
4
+ data.tar.gz: 551313de97c99e736c826248fc9d3ef67afb980d97c43fa44bb20566b9c3c26d
5
5
  SHA512:
6
- metadata.gz: c3b35f154d84a650010b67c70de048ea78b741bd9d6c00fb5543f87eeb1d73f8996b33a027f441fdf861a96142b8e6b4ebbd005bfbf3d8090207df3a4a564984
7
- data.tar.gz: 650bcb9717ac7a4221622f0eadb6549687d3a49fc16f57d99c0d9008acf07649ec34bacab9805d560dcc58070942cc3be92063ccdec7b37aaccdfa50476819ee
6
+ metadata.gz: 05572dc6a8e17492bd9df3d4d1bf1ff4af5e4fccc35d3309b1cef11f7561adcbb1b5f07ab35e723c1780e96fc88f44dd737cc9d25f2b2298a2ec5b578a65e154
7
+ data.tar.gz: 7e55b40a56b7bf861f205308490f3a37036375f93465b54ffeb10032154af84ea1b7110ce31c353bb1cb344d1bcf95f7e5383cfb6c6d9c12caad65e74e137171
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmSDK
4
- VERSION = "2.5.1"
4
+ VERSION = "2.5.2"
5
5
  end
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.1
4
+ version: 2.5.2
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