claude_swarm 1.0.0 → 1.0.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.
Files changed (267) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/release.md +1 -1
  3. data/.claude/hooks/lint-code-files.rb +65 -0
  4. data/.rubocop.yml +22 -2
  5. data/CHANGELOG.md +21 -1
  6. data/CLAUDE.md +1 -1
  7. data/CONTRIBUTING.md +69 -0
  8. data/README.md +27 -2
  9. data/Rakefile +71 -3
  10. data/analyze_coverage.rb +94 -0
  11. data/docs/v2/CHANGELOG.swarm_cli.md +43 -0
  12. data/docs/v2/CHANGELOG.swarm_memory.md +379 -0
  13. data/docs/v2/CHANGELOG.swarm_sdk.md +362 -0
  14. data/docs/v2/README.md +308 -0
  15. data/docs/v2/guides/claude-code-agents.md +262 -0
  16. data/docs/v2/guides/complete-tutorial.md +3088 -0
  17. data/docs/v2/guides/getting-started.md +1456 -0
  18. data/docs/v2/guides/memory-adapters.md +998 -0
  19. data/docs/v2/guides/plugins.md +816 -0
  20. data/docs/v2/guides/quick-start-cli.md +1745 -0
  21. data/docs/v2/guides/rails-integration.md +1902 -0
  22. data/docs/v2/guides/swarm-memory.md +599 -0
  23. data/docs/v2/reference/cli.md +729 -0
  24. data/docs/v2/reference/ruby-dsl.md +2154 -0
  25. data/docs/v2/reference/yaml.md +1835 -0
  26. data/docs-team-swarm.yml +2222 -0
  27. data/examples/learning-assistant/assistant.md +7 -0
  28. data/examples/learning-assistant/example-memories/concept-example.md +90 -0
  29. data/examples/learning-assistant/example-memories/experience-example.md +66 -0
  30. data/examples/learning-assistant/example-memories/fact-example.md +76 -0
  31. data/examples/learning-assistant/example-memories/memory-index.md +78 -0
  32. data/examples/learning-assistant/example-memories/skill-example.md +168 -0
  33. data/examples/learning-assistant/learning_assistant.rb +34 -0
  34. data/examples/learning-assistant/learning_assistant.yml +20 -0
  35. data/examples/v2/dsl/01_basic.rb +44 -0
  36. data/examples/v2/dsl/02_core_parameters.rb +59 -0
  37. data/examples/v2/dsl/03_capabilities.rb +71 -0
  38. data/examples/v2/dsl/04_llm_parameters.rb +56 -0
  39. data/examples/v2/dsl/05_advanced_flags.rb +73 -0
  40. data/examples/v2/dsl/06_permissions.rb +80 -0
  41. data/examples/v2/dsl/07_mcp_server.rb +62 -0
  42. data/examples/v2/dsl/08_swarm_hooks.rb +53 -0
  43. data/examples/v2/dsl/09_agent_hooks.rb +67 -0
  44. data/examples/v2/dsl/10_all_agents_hooks.rb +67 -0
  45. data/examples/v2/dsl/11_delegation.rb +60 -0
  46. data/examples/v2/dsl/12_complete_integration.rb +137 -0
  47. data/examples/v2/file_tools_swarm.yml +102 -0
  48. data/examples/v2/hooks/01_basic_hooks.rb +133 -0
  49. data/examples/v2/hooks/02_usage_tracking.rb +201 -0
  50. data/examples/v2/hooks/03_production_monitoring.rb +429 -0
  51. data/examples/v2/hooks/agent_stop_exit_0.yml +21 -0
  52. data/examples/v2/hooks/agent_stop_exit_1.yml +21 -0
  53. data/examples/v2/hooks/agent_stop_exit_2.yml +26 -0
  54. data/examples/v2/hooks/multiple_hooks_all_pass.yml +37 -0
  55. data/examples/v2/hooks/multiple_hooks_first_fails.yml +37 -0
  56. data/examples/v2/hooks/multiple_hooks_second_fails.yml +37 -0
  57. data/examples/v2/hooks/multiple_hooks_warnings.yml +37 -0
  58. data/examples/v2/hooks/post_tool_use_exit_0.yml +24 -0
  59. data/examples/v2/hooks/post_tool_use_exit_1.yml +24 -0
  60. data/examples/v2/hooks/post_tool_use_exit_2.yml +24 -0
  61. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_0.yml +26 -0
  62. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_1.yml +26 -0
  63. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_2.yml +26 -0
  64. data/examples/v2/hooks/pre_tool_use_exit_0.yml +24 -0
  65. data/examples/v2/hooks/pre_tool_use_exit_1.yml +24 -0
  66. data/examples/v2/hooks/pre_tool_use_exit_2.yml +24 -0
  67. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_0.yml +26 -0
  68. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_1.yml +26 -0
  69. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_2.yml +27 -0
  70. data/examples/v2/hooks/swarm_summary.sh +44 -0
  71. data/examples/v2/hooks/user_prompt_exit_0.yml +21 -0
  72. data/examples/v2/hooks/user_prompt_exit_1.yml +21 -0
  73. data/examples/v2/hooks/user_prompt_exit_2.yml +21 -0
  74. data/examples/v2/hooks/validate_bash.rb +59 -0
  75. data/examples/v2/multi_directory_permissions.yml +221 -0
  76. data/examples/v2/node_context_demo.rb +127 -0
  77. data/examples/v2/node_workflow.rb +173 -0
  78. data/examples/v2/path_resolution_demo.rb +216 -0
  79. data/examples/v2/simple-swarm-v2.rb +90 -0
  80. data/examples/v2/simple-swarm-v2.yml +62 -0
  81. data/examples/v2/swarm.yml +71 -0
  82. data/examples/v2/swarm_with_hooks.yml +61 -0
  83. data/examples/v2/swarm_with_hooks_simple.yml +25 -0
  84. data/examples/v2/think_tool_demo.rb +62 -0
  85. data/exe/swarm +6 -0
  86. data/lib/claude_swarm/claude_mcp_server.rb +0 -6
  87. data/lib/claude_swarm/cli.rb +10 -3
  88. data/lib/claude_swarm/commands/ps.rb +19 -20
  89. data/lib/claude_swarm/commands/show.rb +1 -1
  90. data/lib/claude_swarm/configuration.rb +10 -12
  91. data/lib/claude_swarm/mcp_generator.rb +10 -1
  92. data/lib/claude_swarm/orchestrator.rb +73 -49
  93. data/lib/claude_swarm/system_utils.rb +37 -11
  94. data/lib/claude_swarm/version.rb +1 -1
  95. data/lib/claude_swarm/worktree_manager.rb +1 -0
  96. data/lib/claude_swarm/yaml_loader.rb +22 -0
  97. data/lib/claude_swarm.rb +7 -3
  98. data/lib/swarm_cli/cli.rb +201 -0
  99. data/lib/swarm_cli/command_registry.rb +61 -0
  100. data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
  101. data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
  102. data/lib/swarm_cli/commands/migrate.rb +55 -0
  103. data/lib/swarm_cli/commands/run.rb +173 -0
  104. data/lib/swarm_cli/config_loader.rb +97 -0
  105. data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
  106. data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
  107. data/lib/swarm_cli/interactive_repl.rb +918 -0
  108. data/lib/swarm_cli/mcp_serve_options.rb +44 -0
  109. data/lib/swarm_cli/mcp_tools_options.rb +59 -0
  110. data/lib/swarm_cli/migrate_options.rb +54 -0
  111. data/lib/swarm_cli/migrator.rb +132 -0
  112. data/lib/swarm_cli/options.rb +151 -0
  113. data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
  114. data/lib/swarm_cli/ui/components/content_block.rb +120 -0
  115. data/lib/swarm_cli/ui/components/divider.rb +57 -0
  116. data/lib/swarm_cli/ui/components/panel.rb +62 -0
  117. data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
  118. data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
  119. data/lib/swarm_cli/ui/formatters/number.rb +58 -0
  120. data/lib/swarm_cli/ui/formatters/text.rb +77 -0
  121. data/lib/swarm_cli/ui/formatters/time.rb +73 -0
  122. data/lib/swarm_cli/ui/icons.rb +59 -0
  123. data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
  124. data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
  125. data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
  126. data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
  127. data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
  128. data/lib/swarm_cli/version.rb +5 -0
  129. data/lib/swarm_cli.rb +44 -0
  130. data/lib/swarm_memory/adapters/base.rb +141 -0
  131. data/lib/swarm_memory/adapters/filesystem_adapter.rb +845 -0
  132. data/lib/swarm_memory/chat_extension.rb +34 -0
  133. data/lib/swarm_memory/cli/commands.rb +306 -0
  134. data/lib/swarm_memory/core/entry.rb +37 -0
  135. data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
  136. data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
  137. data/lib/swarm_memory/core/path_normalizer.rb +75 -0
  138. data/lib/swarm_memory/core/semantic_index.rb +244 -0
  139. data/lib/swarm_memory/core/storage.rb +288 -0
  140. data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
  141. data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
  142. data/lib/swarm_memory/dsl/memory_config.rb +113 -0
  143. data/lib/swarm_memory/embeddings/embedder.rb +36 -0
  144. data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
  145. data/lib/swarm_memory/errors.rb +21 -0
  146. data/lib/swarm_memory/integration/cli_registration.rb +30 -0
  147. data/lib/swarm_memory/integration/configuration.rb +43 -0
  148. data/lib/swarm_memory/integration/registration.rb +31 -0
  149. data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
  150. data/lib/swarm_memory/optimization/analyzer.rb +244 -0
  151. data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
  152. data/lib/swarm_memory/prompts/memory.md.erb +109 -0
  153. data/lib/swarm_memory/prompts/memory_assistant.md.erb +181 -0
  154. data/lib/swarm_memory/prompts/memory_researcher.md.erb +281 -0
  155. data/lib/swarm_memory/prompts/memory_retrieval.md.erb +78 -0
  156. data/lib/swarm_memory/search/semantic_search.rb +112 -0
  157. data/lib/swarm_memory/search/text_search.rb +42 -0
  158. data/lib/swarm_memory/search/text_similarity.rb +80 -0
  159. data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
  160. data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
  161. data/lib/swarm_memory/tools/load_skill.rb +313 -0
  162. data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
  163. data/lib/swarm_memory/tools/memory_delete.rb +99 -0
  164. data/lib/swarm_memory/tools/memory_edit.rb +185 -0
  165. data/lib/swarm_memory/tools/memory_glob.rb +160 -0
  166. data/lib/swarm_memory/tools/memory_grep.rb +247 -0
  167. data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
  168. data/lib/swarm_memory/tools/memory_read.rb +123 -0
  169. data/lib/swarm_memory/tools/memory_write.rb +231 -0
  170. data/lib/swarm_memory/utils.rb +50 -0
  171. data/lib/swarm_memory/version.rb +5 -0
  172. data/lib/swarm_memory.rb +166 -0
  173. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
  174. data/lib/swarm_sdk/agent/builder.rb +461 -0
  175. data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
  176. data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
  177. data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
  178. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
  179. data/lib/swarm_sdk/agent/chat.rb +1159 -0
  180. data/lib/swarm_sdk/agent/context.rb +112 -0
  181. data/lib/swarm_sdk/agent/context_manager.rb +309 -0
  182. data/lib/swarm_sdk/agent/definition.rb +556 -0
  183. data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
  184. data/lib/swarm_sdk/configuration.rb +296 -0
  185. data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
  186. data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
  187. data/lib/swarm_sdk/context_compactor.rb +340 -0
  188. data/lib/swarm_sdk/hooks/adapter.rb +359 -0
  189. data/lib/swarm_sdk/hooks/context.rb +197 -0
  190. data/lib/swarm_sdk/hooks/definition.rb +80 -0
  191. data/lib/swarm_sdk/hooks/error.rb +29 -0
  192. data/lib/swarm_sdk/hooks/executor.rb +146 -0
  193. data/lib/swarm_sdk/hooks/registry.rb +147 -0
  194. data/lib/swarm_sdk/hooks/result.rb +150 -0
  195. data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
  196. data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
  197. data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
  198. data/lib/swarm_sdk/log_collector.rb +51 -0
  199. data/lib/swarm_sdk/log_stream.rb +69 -0
  200. data/lib/swarm_sdk/markdown_parser.rb +75 -0
  201. data/lib/swarm_sdk/model_aliases.json +5 -0
  202. data/lib/swarm_sdk/models.json +1 -0
  203. data/lib/swarm_sdk/models.rb +120 -0
  204. data/lib/swarm_sdk/node/agent_config.rb +49 -0
  205. data/lib/swarm_sdk/node/builder.rb +439 -0
  206. data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
  207. data/lib/swarm_sdk/node_context.rb +170 -0
  208. data/lib/swarm_sdk/node_orchestrator.rb +384 -0
  209. data/lib/swarm_sdk/permissions/config.rb +239 -0
  210. data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
  211. data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
  212. data/lib/swarm_sdk/permissions/validator.rb +173 -0
  213. data/lib/swarm_sdk/permissions_builder.rb +122 -0
  214. data/lib/swarm_sdk/plugin.rb +147 -0
  215. data/lib/swarm_sdk/plugin_registry.rb +101 -0
  216. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
  217. data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
  218. data/lib/swarm_sdk/result.rb +97 -0
  219. data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
  220. data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
  221. data/lib/swarm_sdk/swarm/builder.rb +586 -0
  222. data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
  223. data/lib/swarm_sdk/swarm/tool_configurator.rb +419 -0
  224. data/lib/swarm_sdk/swarm.rb +982 -0
  225. data/lib/swarm_sdk/tools/bash.rb +274 -0
  226. data/lib/swarm_sdk/tools/clock.rb +44 -0
  227. data/lib/swarm_sdk/tools/delegate.rb +164 -0
  228. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
  229. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
  230. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
  231. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
  232. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
  233. data/lib/swarm_sdk/tools/edit.rb +150 -0
  234. data/lib/swarm_sdk/tools/glob.rb +158 -0
  235. data/lib/swarm_sdk/tools/grep.rb +228 -0
  236. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
  237. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
  238. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
  239. data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
  240. data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
  241. data/lib/swarm_sdk/tools/read.rb +251 -0
  242. data/lib/swarm_sdk/tools/registry.rb +93 -0
  243. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
  244. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
  245. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
  246. data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
  247. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
  248. data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
  249. data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
  250. data/lib/swarm_sdk/tools/think.rb +95 -0
  251. data/lib/swarm_sdk/tools/todo_write.rb +216 -0
  252. data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
  253. data/lib/swarm_sdk/tools/write.rb +117 -0
  254. data/lib/swarm_sdk/utils.rb +50 -0
  255. data/lib/swarm_sdk/version.rb +5 -0
  256. data/lib/swarm_sdk.rb +157 -0
  257. data/llm.v2.txt +13407 -0
  258. data/rubocop/cop/security/no_reflection_methods.rb +47 -0
  259. data/rubocop/cop/security/no_ruby_llm_logger.rb +32 -0
  260. data/swarm_cli.gemspec +57 -0
  261. data/swarm_memory.gemspec +28 -0
  262. data/swarm_sdk.gemspec +41 -0
  263. data/team.yml +1 -1
  264. data/team_full.yml +1875 -0
  265. data/{team_v2.yml → team_sdk.yml} +121 -52
  266. metadata +247 -4
  267. data/EXAMPLES.md +0 -164
@@ -0,0 +1,314 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Agent
5
+ class Chat < RubyLLM::Chat
6
+ # Manages context tracking, delegation tracking, and logging callbacks
7
+ #
8
+ # Responsibilities:
9
+ # - Register RubyLLM callbacks for logging
10
+ # - Track tool executions
11
+ # - Track delegations (which tool calls are delegations)
12
+ # - Emit log events via LogStream
13
+ # - Check context warnings
14
+ #
15
+ # This is a stateful helper that's instantiated per Agent::Chat instance.
16
+ class ContextTracker
17
+ include LoggingHelpers
18
+
19
+ attr_reader :agent_context
20
+
21
+ def initialize(chat, agent_context)
22
+ @chat = chat
23
+ @agent_context = agent_context
24
+ @tool_executions = []
25
+ @finish_reason_override = nil
26
+ end
27
+
28
+ # Set a custom finish reason for the next agent_stop event
29
+ #
30
+ # This is used when finish_agent or finish_swarm terminates execution early.
31
+ #
32
+ # @param reason [String] Custom finish reason (e.g., "finish_agent", "finish_swarm")
33
+ attr_writer :finish_reason_override
34
+
35
+ # Setup logging callbacks
36
+ #
37
+ # Registers RubyLLM callbacks to collect data and emit log events.
38
+ # Should only be called when LogStream.emitter is set.
39
+ #
40
+ # @return [void]
41
+ def setup_logging
42
+ register_logging_callbacks
43
+ end
44
+
45
+ # Extract agent name from delegation tool name
46
+ #
47
+ # Converts "DelegateTaskTo[AgentName]" to "agent_name"
48
+ # Example: "DelegateTaskToWorker" -> "worker"
49
+ #
50
+ # @param tool_name [String] Delegation tool name
51
+ # @return [String] Agent name
52
+ def extract_delegate_agent_name(tool_name)
53
+ # Remove "DelegateTaskTo" prefix and lowercase first letter
54
+ agent_name = tool_name.to_s.sub(/^DelegateTaskTo/, "")
55
+ # Convert from PascalCase to lowercase (e.g., "Worker" -> "worker", "BackendDev" -> "backendDev")
56
+ agent_name[0] = agent_name[0].downcase unless agent_name.empty?
57
+ agent_name
58
+ end
59
+
60
+ # Check if context usage has crossed warning thresholds and emit warnings
61
+ #
62
+ # This should be called after each LLM response to check if we've crossed
63
+ # any warning thresholds (80%, 90%, etc.)
64
+ #
65
+ # @return [void]
66
+ def check_context_warnings
67
+ current_percentage = @chat.context_usage_percentage
68
+
69
+ Context::CONTEXT_WARNING_THRESHOLDS.each do |threshold|
70
+ # Only warn once per threshold
71
+ next if @agent_context.warning_threshold_hit?(threshold)
72
+ next if current_percentage < threshold
73
+
74
+ # Mark threshold as hit and emit warning
75
+ @agent_context.hit_warning_threshold?(threshold)
76
+
77
+ # Trigger automatic compression at 60% threshold
78
+ if threshold == Context::COMPRESSION_THRESHOLD
79
+ trigger_automatic_compression
80
+ end
81
+
82
+ LogStream.emit(
83
+ type: "context_limit_warning",
84
+ agent: @agent_context.name,
85
+ model: @chat.model.id,
86
+ threshold: "#{threshold}%",
87
+ current_usage: "#{current_percentage}%",
88
+ tokens_used: @chat.cumulative_total_tokens,
89
+ tokens_remaining: @chat.tokens_remaining,
90
+ context_limit: @chat.context_limit,
91
+ metadata: @agent_context.metadata,
92
+ compression_triggered: threshold == Context::COMPRESSION_THRESHOLD,
93
+ )
94
+ end
95
+ end
96
+
97
+ private
98
+
99
+ # Extract usage information from an assistant message
100
+ #
101
+ # @param message [RubyLLM::Message] Assistant message with usage data
102
+ # @return [Hash] Usage information
103
+ def extract_usage_info(message)
104
+ cost_info = calculate_cost(message)
105
+ context_usage = if @chat.respond_to?(:cumulative_input_tokens)
106
+ {
107
+ cumulative_input_tokens: @chat.cumulative_input_tokens,
108
+ cumulative_output_tokens: @chat.cumulative_output_tokens,
109
+ cumulative_total_tokens: @chat.cumulative_total_tokens,
110
+ context_limit: @chat.context_limit,
111
+ tokens_used_percentage: "#{@chat.context_usage_percentage}%",
112
+ tokens_remaining: @chat.tokens_remaining,
113
+ }
114
+ else
115
+ {}
116
+ end
117
+
118
+ {
119
+ input_tokens: message.input_tokens,
120
+ output_tokens: message.output_tokens,
121
+ total_tokens: (message.input_tokens || 0) + (message.output_tokens || 0),
122
+ input_cost: cost_info[:input_cost],
123
+ output_cost: cost_info[:output_cost],
124
+ total_cost: cost_info[:total_cost],
125
+ }.merge(context_usage)
126
+ end
127
+
128
+ # Register RubyLLM chat callbacks to collect data and trigger logging
129
+ #
130
+ # This sets up low-level RubyLLM callbacks for technical plumbing (tracking state,
131
+ # collecting tool results), then emits log events via LogStream.
132
+ #
133
+ # @return [void]
134
+ def register_logging_callbacks
135
+ # Collect tool execution results (technical plumbing)
136
+ @chat.on_tool_result do |result|
137
+ @tool_executions << {
138
+ result: serialize_result(result),
139
+ completed_at: Time.now.utc.iso8601,
140
+ }
141
+ end
142
+
143
+ # Track delegations and emit agent_step/agent_stop events
144
+ @chat.on_end_message do |message|
145
+ next unless message
146
+
147
+ case message.role
148
+ when :assistant
149
+ if message.tool_call?
150
+ # Assistant made tool calls - emit agent_step event
151
+ trigger_agent_step(message, tool_executions: @tool_executions) if @chat.hook_executor
152
+ @tool_executions.clear
153
+ elsif @chat.hook_executor
154
+ # Final response (finish_reason: "stop") - fire agent_stop
155
+ trigger_agent_stop(message, tool_executions: @tool_executions)
156
+ end
157
+ when :tool
158
+ # Handle delegation tracking and logging (technical plumbing)
159
+ if @agent_context.delegation?(call_id: message.tool_call_id)
160
+ delegate_from = @agent_context.delegation_target(call_id: message.tool_call_id)
161
+
162
+ # Emit delegation result log event
163
+ LogStream.emit(
164
+ type: "delegation_result",
165
+ agent: @agent_context.name,
166
+ delegate_from: delegate_from,
167
+ tool_call_id: message.tool_call_id,
168
+ result: serialize_result(message.content),
169
+ metadata: @agent_context.metadata,
170
+ )
171
+
172
+ @agent_context.clear_delegation(call_id: message.tool_call_id)
173
+ end
174
+ end
175
+ end
176
+
177
+ # Track delegations when tool calls are made
178
+ @chat.on_tool_call do |tool_call|
179
+ if @agent_context.delegation_tool?(tool_call.name)
180
+ # Extract agent name from tool name (DelegateTaskTo[AgentName] -> agent_name)
181
+ agent_name = extract_delegate_agent_name(tool_call.name)
182
+
183
+ @agent_context.track_delegation(call_id: tool_call.id, target: agent_name)
184
+
185
+ # Emit delegation log event
186
+ LogStream.emit(
187
+ type: "agent_delegation",
188
+ agent: @agent_context.name,
189
+ tool_call_id: tool_call.id,
190
+ delegate_to: agent_name,
191
+ arguments: tool_call.arguments,
192
+ metadata: @agent_context.metadata,
193
+ )
194
+ end
195
+ end
196
+ end
197
+
198
+ # Trigger agent_step callback
199
+ #
200
+ # This fires when the agent makes an intermediate response with tool calls.
201
+ # The agent hasn't finished yet - it's requesting tools to continue processing.
202
+ #
203
+ # @param message [RubyLLM::Message] Assistant message with tool calls
204
+ # @param tool_executions [Array<Hash>] Tool execution results (should be empty for steps)
205
+ # @return [void]
206
+ def trigger_agent_step(message, tool_executions: [])
207
+ return unless @chat.hook_executor
208
+
209
+ usage_info = extract_usage_info(message)
210
+
211
+ context = Hooks::Context.new(
212
+ event: :agent_step,
213
+ agent_name: @agent_context.name,
214
+ swarm: @chat.hook_swarm,
215
+ metadata: {
216
+ model: message.model_id,
217
+ content: message.content,
218
+ tool_calls: format_tool_calls(message.tool_calls),
219
+ finish_reason: "tool_calls",
220
+ usage: usage_info,
221
+ tool_executions: tool_executions.empty? ? nil : tool_executions,
222
+ timestamp: Time.now.utc.iso8601,
223
+ },
224
+ )
225
+
226
+ agent_hooks = @chat.hook_agent_hooks[:agent_step] || []
227
+
228
+ @chat.hook_executor.execute_safe(
229
+ event: :agent_step,
230
+ context: context,
231
+ callbacks: agent_hooks,
232
+ )
233
+ end
234
+
235
+ # Trigger agent_stop callback
236
+ #
237
+ # This fires when the agent completes with a final response (no more tool calls).
238
+ #
239
+ # @param message [RubyLLM::Message] Assistant message with final content
240
+ # @param tool_executions [Array<Hash>] Tool execution results (if any)
241
+ # @return [void]
242
+ def trigger_agent_stop(message, tool_executions: [])
243
+ return unless @chat.hook_executor
244
+
245
+ usage_info = extract_usage_info(message)
246
+
247
+ # Use override if set (e.g., "finish_agent"), otherwise default to "stop"
248
+ finish_reason = @finish_reason_override || "stop"
249
+ @finish_reason_override = nil # Clear after use
250
+
251
+ context = Hooks::Context.new(
252
+ event: :agent_stop,
253
+ agent_name: @agent_context.name,
254
+ swarm: @chat.hook_swarm,
255
+ metadata: {
256
+ model: message.model_id,
257
+ content: message.content,
258
+ tool_calls: nil, # Final response has no tool calls
259
+ finish_reason: finish_reason,
260
+ usage: usage_info,
261
+ tool_executions: tool_executions.empty? ? nil : tool_executions,
262
+ timestamp: Time.now.utc.iso8601,
263
+ },
264
+ )
265
+
266
+ agent_hooks = @chat.hook_agent_hooks[:agent_stop] || []
267
+
268
+ @chat.hook_executor.execute_safe(
269
+ event: :agent_stop,
270
+ context: context,
271
+ callbacks: agent_hooks,
272
+ )
273
+ end
274
+ end
275
+
276
+ # Trigger automatic message compression
277
+ #
278
+ # Called when context usage crosses 60% threshold. Compresses old tool
279
+ # results to save context window space while preserving accuracy.
280
+ #
281
+ # @return [void]
282
+ def trigger_automatic_compression
283
+ return unless @chat.respond_to?(:context_manager)
284
+
285
+ # Calculate tokens before compression
286
+ tokens_before = @chat.cumulative_total_tokens
287
+
288
+ # Get compressed messages from ContextManager
289
+ compressed = @chat.context_manager.auto_compress_on_threshold(@chat.messages, keep_recent: 10)
290
+
291
+ # Count how many messages were actually compressed
292
+ messages_compressed = compressed.count do |msg|
293
+ msg.content.to_s.include?("[truncated for context management]")
294
+ end
295
+
296
+ # Replace messages array with compressed version
297
+ @chat.messages.clear
298
+ compressed.each { |msg| @chat.messages << msg }
299
+
300
+ # Log compression event
301
+ LogStream.emit(
302
+ type: "context_compression",
303
+ agent: @agent_context.name,
304
+ total_messages: @chat.messages.size,
305
+ messages_compressed: messages_compressed,
306
+ tokens_before: tokens_before,
307
+ current_usage: "#{@chat.context_usage_percentage}%",
308
+ compression_strategy: "progressive_tool_result_compression",
309
+ keep_recent: 10,
310
+ ) if LogStream.enabled?
311
+ end
312
+ end
313
+ end
314
+ end
@@ -0,0 +1,372 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Agent
5
+ class Chat < RubyLLM::Chat
6
+ # Integrates SwarmSDK's hook system with Agent::Chat
7
+ #
8
+ # Responsibilities:
9
+ # - Setup hook system (registry, executor, agent hooks)
10
+ # - Provide trigger methods for all hook events
11
+ # - Wrap ask() to inject user_prompt hooks
12
+ # - Handle hook results (halt, replace, continue, reprompt)
13
+ #
14
+ # This module is included in Agent::Chat and provides methods for triggering hooks.
15
+ # It overrides ask() to inject user_prompt hooks, but does NOT override
16
+ # handle_tool_calls (that's handled in Agent::Chat with explicit hook calls).
17
+ module HookIntegration
18
+ # Expose hook system components for ContextTracker
19
+ attr_reader :hook_executor, :hook_swarm, :hook_agent_hooks
20
+
21
+ # Setup the hook system for this agent chat
22
+ #
23
+ # This must be called after setup_context and before the first ask/complete.
24
+ # It wires up the hook system to trigger at the right times.
25
+ #
26
+ # @param registry [Hooks::Registry] Shared registry for named hooks and swarm defaults
27
+ # @param agent_definition [Agent::Definition] Agent configuration with hooks
28
+ # @param swarm [Swarm, nil] Reference to swarm for context
29
+ # @return [void]
30
+ def setup_hooks(registry:, agent_definition:, swarm: nil)
31
+ @hook_registry = registry
32
+ @hook_swarm = swarm
33
+ @hook_executor = Hooks::Executor.new(registry, logger: RubyLLM.logger)
34
+
35
+ # Extract agent hooks based on format
36
+ hooks = agent_definition.hooks || {}
37
+
38
+ # Check if hooks are pre-parsed HookDefinition objects (from DSL)
39
+ # or raw YAML hash (to be processed by Hooks::Adapter in pass_5)
40
+ @hook_agent_hooks = if hooks.is_a?(Hash) && hooks.values.all? { |v| v.is_a?(Array) && v.all? { |item| item.is_a?(Hooks::Definition) } }
41
+ # DSL hooks - already parsed, use them
42
+ hooks
43
+ else
44
+ # YAML hooks - raw hash, will be processed in pass_5 by Hooks::Adapter
45
+ # For now, use empty hash (pass_5 will add them later)
46
+ {}
47
+ end
48
+ end
49
+
50
+ # Add a hook programmatically at runtime
51
+ #
52
+ # This allows agents to add hooks dynamically, which is useful for
53
+ # implementing adaptive behavior or runtime monitoring.
54
+ #
55
+ # @param event [Symbol] Event type (e.g., :pre_tool_use)
56
+ # @param matcher [String, Regexp, nil] Optional regex pattern for tool names
57
+ # @param priority [Integer] Execution priority (higher = earlier)
58
+ # @param block [Proc] Hook implementation
59
+ def add_hook(event, matcher: nil, priority: 0, &block)
60
+ raise ArgumentError, "Hooks not set up. Call setup_hooks first." unless @hook_executor
61
+
62
+ definition = Hooks::Definition.new(
63
+ event: event,
64
+ matcher: matcher,
65
+ priority: priority,
66
+ proc: block,
67
+ )
68
+
69
+ @hook_agent_hooks[event] ||= []
70
+ @hook_agent_hooks[event] << definition
71
+ @hook_agent_hooks[event].sort_by! { |cb| -cb.priority }
72
+ end
73
+
74
+ # Override ask to trigger user_prompt hooks
75
+ #
76
+ # This wraps the Agent::Chat#ask implementation to inject hooks AFTER
77
+ # system reminders are handled.
78
+ #
79
+ # @param prompt [String] User prompt
80
+ # @param options [Hash] Additional options
81
+ # @return [RubyLLM::Message] LLM response
82
+ def ask(prompt, **options)
83
+ # Trigger user_prompt hook before sending to LLM (can halt or modify prompt)
84
+ if @hook_executor
85
+ hook_result = trigger_user_prompt(prompt)
86
+
87
+ # Check if hook halted execution
88
+ if hook_result[:halted]
89
+ # Return a halted message instead of calling LLM
90
+ return RubyLLM::Message.new(
91
+ role: :assistant,
92
+ content: hook_result[:halt_message],
93
+ model_id: model.id,
94
+ )
95
+ end
96
+
97
+ # Use modified prompt if hook provided one (stdout injection)
98
+ prompt = hook_result[:modified_prompt] if hook_result[:modified_prompt]
99
+ end
100
+
101
+ # Call original ask implementation (Agent::Chat handles system reminders)
102
+ super(prompt, **options)
103
+ end
104
+
105
+ # Override check_context_warnings to trigger our hook system
106
+ #
107
+ # This wraps the default context warning behavior to also trigger hooks.
108
+ def check_context_warnings
109
+ return unless respond_to?(:context_usage_percentage)
110
+
111
+ current_percentage = context_usage_percentage
112
+
113
+ Context::CONTEXT_WARNING_THRESHOLDS.each do |threshold|
114
+ # Only warn once per threshold
115
+ next if @agent_context.warning_threshold_hit?(threshold)
116
+ next if current_percentage < threshold
117
+
118
+ # Mark threshold as hit
119
+ @agent_context.hit_warning_threshold?(threshold)
120
+
121
+ # Emit existing log event (for backward compatibility)
122
+ LogStream.emit(
123
+ type: "context_limit_warning",
124
+ agent: @agent_context.name,
125
+ model: model.id,
126
+ threshold: "#{threshold}%",
127
+ current_usage: "#{current_percentage}%",
128
+ tokens_used: cumulative_total_tokens,
129
+ tokens_remaining: tokens_remaining,
130
+ context_limit: context_limit,
131
+ metadata: @agent_context.metadata,
132
+ )
133
+
134
+ # Trigger hook system
135
+ trigger_context_warning(threshold, current_percentage) if @hook_executor
136
+ end
137
+ end
138
+
139
+ # Trigger pre_tool_use hooks
140
+ #
141
+ # Should be called by Agent::Chat before tool execution.
142
+ # Returns a hash indicating whether to proceed and any custom result.
143
+ #
144
+ # @param tool_call [RubyLLM::ToolCall] Tool call from LLM
145
+ # @return [Hash] { proceed: true/false, custom_result: result (if any) }
146
+ def trigger_pre_tool_use(tool_call)
147
+ return { proceed: true } unless @hook_executor
148
+
149
+ context = build_hook_context(
150
+ event: :pre_tool_use,
151
+ tool_call: wrap_tool_call_to_hooks(tool_call),
152
+ )
153
+
154
+ agent_hooks = @hook_agent_hooks[:pre_tool_use] || []
155
+
156
+ result = @hook_executor.execute_safe(
157
+ event: :pre_tool_use,
158
+ context: context,
159
+ callbacks: agent_hooks,
160
+ )
161
+
162
+ # Return custom result if hook provides one
163
+ if result.replace?
164
+ { proceed: false, custom_result: result.value }
165
+ elsif result.halt?
166
+ { proceed: false, custom_result: result.value || "Tool execution blocked by hook" }
167
+ elsif result.finish_agent?
168
+ # Finish agent execution immediately with this message
169
+ { proceed: false, finish_agent: true, custom_result: result.value }
170
+ elsif result.finish_swarm?
171
+ # Finish entire swarm execution immediately with this message
172
+ { proceed: false, finish_swarm: true, custom_result: result.value }
173
+ else
174
+ { proceed: true }
175
+ end
176
+ end
177
+
178
+ # Trigger post_tool_use hooks
179
+ #
180
+ # Should be called by Agent::Chat after tool execution.
181
+ # Returns modified result if hook replaces it, or a special marker for finish actions.
182
+ #
183
+ # @param result [String, Object] Tool execution result
184
+ # @param tool_call [RubyLLM::ToolCall] Tool call object with full context
185
+ # @return [Object, Hash] Modified result if hook replaces it, hash with :finish_agent or :finish_swarm if finishing, otherwise original result
186
+ def trigger_post_tool_use(result, tool_call:)
187
+ return result unless @hook_executor
188
+
189
+ context = build_hook_context(
190
+ event: :post_tool_use,
191
+ tool_result: wrap_tool_result(tool_call.id, tool_call.name, result),
192
+ )
193
+
194
+ agent_hooks = @hook_agent_hooks[:post_tool_use] || []
195
+
196
+ hook_result = @hook_executor.execute_safe(
197
+ event: :post_tool_use,
198
+ context: context,
199
+ callbacks: agent_hooks,
200
+ )
201
+
202
+ # Return modified result or finish markers
203
+ if hook_result.replace?
204
+ hook_result.value
205
+ elsif hook_result.finish_agent?
206
+ { __finish_agent__: true, message: hook_result.value }
207
+ elsif hook_result.finish_swarm?
208
+ { __finish_swarm__: true, message: hook_result.value }
209
+ else
210
+ result
211
+ end
212
+ end
213
+
214
+ private
215
+
216
+ # Trigger context_warning hooks
217
+ #
218
+ # Hooks have access to the chat instance via metadata[:chat]
219
+ # to access and manipulate the messages array.
220
+ #
221
+ # @param threshold [Integer] Warning threshold percentage
222
+ # @param current_usage [Float] Current usage percentage
223
+ # @return [void]
224
+ def trigger_context_warning(threshold, current_usage)
225
+ return unless @hook_executor
226
+
227
+ context = build_hook_context(
228
+ event: :context_warning,
229
+ metadata: {
230
+ chat: self, # Provide access to chat instance (for messages array)
231
+ threshold: threshold,
232
+ percentage: current_usage,
233
+ tokens_used: cumulative_total_tokens,
234
+ tokens_remaining: tokens_remaining,
235
+ context_limit: context_limit,
236
+ },
237
+ )
238
+
239
+ agent_hooks = @hook_agent_hooks[:context_warning] || []
240
+
241
+ @hook_executor.execute_safe(
242
+ event: :context_warning,
243
+ context: context,
244
+ callbacks: agent_hooks,
245
+ )
246
+ end
247
+
248
+ # Trigger user_prompt hooks
249
+ #
250
+ # This fires before sending a user message to the LLM.
251
+ # Can halt execution or append hook stdout to prompt.
252
+ #
253
+ # @param prompt [String] User's message/prompt
254
+ # @return [Hash] { halted: bool, halt_message: String, modified_prompt: String }
255
+ def trigger_user_prompt(prompt)
256
+ return { halted: false, modified_prompt: prompt } unless @hook_executor
257
+
258
+ # Filter out delegation tools from tools list
259
+ actual_tools = if respond_to?(:tools) && @agent_context
260
+ tools.keys.reject { |tool_name| @agent_context.delegation_tool?(tool_name.to_s) }
261
+ else
262
+ []
263
+ end
264
+
265
+ # Extract agent names from delegation tool names
266
+ delegate_agents = if @agent_context&.delegation_tools
267
+ @agent_context.delegation_tools.map { |tool_name| @context_tracker.extract_delegate_agent_name(tool_name) }
268
+ else
269
+ []
270
+ end
271
+
272
+ context = build_hook_context(
273
+ event: :user_prompt,
274
+ metadata: {
275
+ prompt: prompt,
276
+ message_count: messages.size,
277
+ model: model.id,
278
+ provider: model.provider,
279
+ tools: actual_tools,
280
+ delegates_to: delegate_agents,
281
+ timestamp: Time.now.utc.iso8601,
282
+ },
283
+ )
284
+
285
+ agent_hooks = @hook_agent_hooks[:user_prompt] || []
286
+
287
+ result = @hook_executor.execute_safe(
288
+ event: :user_prompt,
289
+ context: context,
290
+ callbacks: agent_hooks,
291
+ )
292
+
293
+ # Handle hook result
294
+ if result.halt?
295
+ # Hook blocked execution
296
+ { halted: true, halt_message: result.value }
297
+ elsif result.replace?
298
+ # Hook provided stdout to append to prompt (exit code 0)
299
+ modified_prompt = "#{prompt}\n\n<hook-context>\n#{result.value}\n</hook-context>"
300
+ { halted: false, modified_prompt: modified_prompt }
301
+ else
302
+ # Normal continue
303
+ { halted: false, modified_prompt: prompt }
304
+ end
305
+ end
306
+
307
+ # Build a hook context object
308
+ #
309
+ # @param event [Symbol] Event type
310
+ # @param tool_call [Hooks::ToolCall, nil] Tool call object
311
+ # @param tool_result [Hooks::ToolResult, nil] Tool result object
312
+ # @param metadata [Hash] Additional metadata
313
+ # @return [Hooks::Context] Context object
314
+ def build_hook_context(event:, tool_call: nil, tool_result: nil, metadata: {})
315
+ Hooks::Context.new(
316
+ event: event,
317
+ agent_name: @agent_context&.name || "unknown",
318
+ agent_definition: nil, # Could store this in setup_hooks if needed
319
+ swarm: @hook_swarm,
320
+ tool_call: tool_call,
321
+ tool_result: tool_result,
322
+ metadata: metadata,
323
+ )
324
+ end
325
+
326
+ # Wrap a RubyLLM tool call in our Hooks::ToolCall value object
327
+ #
328
+ # @param tool_call [RubyLLM::ToolCall] RubyLLM tool call
329
+ # @return [Hooks::ToolCall] Our wrapped tool call
330
+ def wrap_tool_call_to_hooks(tool_call)
331
+ Hooks::ToolCall.new(
332
+ id: tool_call.id,
333
+ name: tool_call.name,
334
+ parameters: tool_call.arguments,
335
+ )
336
+ end
337
+
338
+ # Wrap a tool result in our Hooks::ToolResult value object
339
+ #
340
+ # @param tool_call_id [String] Tool call ID
341
+ # @param tool_name [String] Tool name
342
+ # @param result [Object] Tool execution result
343
+ # @return [Hooks::ToolResult] Our wrapped result
344
+ def wrap_tool_result(tool_call_id, tool_name, result)
345
+ success = !result.is_a?(StandardError)
346
+ error = result.is_a?(StandardError) ? result.message : nil
347
+
348
+ Hooks::ToolResult.new(
349
+ tool_call_id: tool_call_id,
350
+ tool_name: tool_name,
351
+ content: success ? result : nil,
352
+ success: success,
353
+ error: error,
354
+ )
355
+ end
356
+
357
+ # Check if a tool call is a delegation tool
358
+ #
359
+ # Delegation tools fire their own pre_delegation/post_delegation events
360
+ # and should NOT fire pre_tool_use/post_tool_use events.
361
+ #
362
+ # @param tool_call [RubyLLM::ToolCall] Tool call to check
363
+ # @return [Boolean] true if this is a delegation tool
364
+ def delegation_tool_call?(tool_call)
365
+ return false unless @agent_context
366
+
367
+ @agent_context.delegation_tool?(tool_call.name)
368
+ end
369
+ end
370
+ end
371
+ end
372
+ end