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.
- checksums.yaml +4 -4
- data/.claude/commands/release.md +1 -1
- data/.claude/hooks/lint-code-files.rb +65 -0
- data/.rubocop.yml +22 -2
- data/CHANGELOG.md +21 -1
- data/CLAUDE.md +1 -1
- data/CONTRIBUTING.md +69 -0
- data/README.md +27 -2
- data/Rakefile +71 -3
- data/analyze_coverage.rb +94 -0
- data/docs/v2/CHANGELOG.swarm_cli.md +43 -0
- data/docs/v2/CHANGELOG.swarm_memory.md +379 -0
- data/docs/v2/CHANGELOG.swarm_sdk.md +362 -0
- data/docs/v2/README.md +308 -0
- data/docs/v2/guides/claude-code-agents.md +262 -0
- data/docs/v2/guides/complete-tutorial.md +3088 -0
- data/docs/v2/guides/getting-started.md +1456 -0
- data/docs/v2/guides/memory-adapters.md +998 -0
- data/docs/v2/guides/plugins.md +816 -0
- data/docs/v2/guides/quick-start-cli.md +1745 -0
- data/docs/v2/guides/rails-integration.md +1902 -0
- data/docs/v2/guides/swarm-memory.md +599 -0
- data/docs/v2/reference/cli.md +729 -0
- data/docs/v2/reference/ruby-dsl.md +2154 -0
- data/docs/v2/reference/yaml.md +1835 -0
- data/docs-team-swarm.yml +2222 -0
- data/examples/learning-assistant/assistant.md +7 -0
- data/examples/learning-assistant/example-memories/concept-example.md +90 -0
- data/examples/learning-assistant/example-memories/experience-example.md +66 -0
- data/examples/learning-assistant/example-memories/fact-example.md +76 -0
- data/examples/learning-assistant/example-memories/memory-index.md +78 -0
- data/examples/learning-assistant/example-memories/skill-example.md +168 -0
- data/examples/learning-assistant/learning_assistant.rb +34 -0
- data/examples/learning-assistant/learning_assistant.yml +20 -0
- data/examples/v2/dsl/01_basic.rb +44 -0
- data/examples/v2/dsl/02_core_parameters.rb +59 -0
- data/examples/v2/dsl/03_capabilities.rb +71 -0
- data/examples/v2/dsl/04_llm_parameters.rb +56 -0
- data/examples/v2/dsl/05_advanced_flags.rb +73 -0
- data/examples/v2/dsl/06_permissions.rb +80 -0
- data/examples/v2/dsl/07_mcp_server.rb +62 -0
- data/examples/v2/dsl/08_swarm_hooks.rb +53 -0
- data/examples/v2/dsl/09_agent_hooks.rb +67 -0
- data/examples/v2/dsl/10_all_agents_hooks.rb +67 -0
- data/examples/v2/dsl/11_delegation.rb +60 -0
- data/examples/v2/dsl/12_complete_integration.rb +137 -0
- data/examples/v2/file_tools_swarm.yml +102 -0
- data/examples/v2/hooks/01_basic_hooks.rb +133 -0
- data/examples/v2/hooks/02_usage_tracking.rb +201 -0
- data/examples/v2/hooks/03_production_monitoring.rb +429 -0
- data/examples/v2/hooks/agent_stop_exit_0.yml +21 -0
- data/examples/v2/hooks/agent_stop_exit_1.yml +21 -0
- data/examples/v2/hooks/agent_stop_exit_2.yml +26 -0
- data/examples/v2/hooks/multiple_hooks_all_pass.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_first_fails.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_second_fails.yml +37 -0
- data/examples/v2/hooks/multiple_hooks_warnings.yml +37 -0
- data/examples/v2/hooks/post_tool_use_exit_0.yml +24 -0
- data/examples/v2/hooks/post_tool_use_exit_1.yml +24 -0
- data/examples/v2/hooks/post_tool_use_exit_2.yml +24 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_0.yml +26 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_1.yml +26 -0
- data/examples/v2/hooks/post_tool_use_multi_matcher_exit_2.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_exit_0.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_exit_1.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_exit_2.yml +24 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_0.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_1.yml +26 -0
- data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_2.yml +27 -0
- data/examples/v2/hooks/swarm_summary.sh +44 -0
- data/examples/v2/hooks/user_prompt_exit_0.yml +21 -0
- data/examples/v2/hooks/user_prompt_exit_1.yml +21 -0
- data/examples/v2/hooks/user_prompt_exit_2.yml +21 -0
- data/examples/v2/hooks/validate_bash.rb +59 -0
- data/examples/v2/multi_directory_permissions.yml +221 -0
- data/examples/v2/node_context_demo.rb +127 -0
- data/examples/v2/node_workflow.rb +173 -0
- data/examples/v2/path_resolution_demo.rb +216 -0
- data/examples/v2/simple-swarm-v2.rb +90 -0
- data/examples/v2/simple-swarm-v2.yml +62 -0
- data/examples/v2/swarm.yml +71 -0
- data/examples/v2/swarm_with_hooks.yml +61 -0
- data/examples/v2/swarm_with_hooks_simple.yml +25 -0
- data/examples/v2/think_tool_demo.rb +62 -0
- data/exe/swarm +6 -0
- data/lib/claude_swarm/claude_mcp_server.rb +0 -6
- data/lib/claude_swarm/cli.rb +10 -3
- data/lib/claude_swarm/commands/ps.rb +19 -20
- data/lib/claude_swarm/commands/show.rb +1 -1
- data/lib/claude_swarm/configuration.rb +10 -12
- data/lib/claude_swarm/mcp_generator.rb +10 -1
- data/lib/claude_swarm/orchestrator.rb +73 -49
- data/lib/claude_swarm/system_utils.rb +37 -11
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm/worktree_manager.rb +1 -0
- data/lib/claude_swarm/yaml_loader.rb +22 -0
- data/lib/claude_swarm.rb +7 -3
- data/lib/swarm_cli/cli.rb +201 -0
- data/lib/swarm_cli/command_registry.rb +61 -0
- data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
- data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
- data/lib/swarm_cli/commands/migrate.rb +55 -0
- data/lib/swarm_cli/commands/run.rb +173 -0
- data/lib/swarm_cli/config_loader.rb +97 -0
- data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
- data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
- data/lib/swarm_cli/interactive_repl.rb +918 -0
- data/lib/swarm_cli/mcp_serve_options.rb +44 -0
- data/lib/swarm_cli/mcp_tools_options.rb +59 -0
- data/lib/swarm_cli/migrate_options.rb +54 -0
- data/lib/swarm_cli/migrator.rb +132 -0
- data/lib/swarm_cli/options.rb +151 -0
- data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
- data/lib/swarm_cli/ui/components/content_block.rb +120 -0
- data/lib/swarm_cli/ui/components/divider.rb +57 -0
- data/lib/swarm_cli/ui/components/panel.rb +62 -0
- data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
- data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
- data/lib/swarm_cli/ui/formatters/number.rb +58 -0
- data/lib/swarm_cli/ui/formatters/text.rb +77 -0
- data/lib/swarm_cli/ui/formatters/time.rb +73 -0
- data/lib/swarm_cli/ui/icons.rb +59 -0
- data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
- data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
- data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
- data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
- data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
- data/lib/swarm_cli/version.rb +5 -0
- data/lib/swarm_cli.rb +44 -0
- data/lib/swarm_memory/adapters/base.rb +141 -0
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +845 -0
- data/lib/swarm_memory/chat_extension.rb +34 -0
- data/lib/swarm_memory/cli/commands.rb +306 -0
- data/lib/swarm_memory/core/entry.rb +37 -0
- data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
- data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
- data/lib/swarm_memory/core/path_normalizer.rb +75 -0
- data/lib/swarm_memory/core/semantic_index.rb +244 -0
- data/lib/swarm_memory/core/storage.rb +288 -0
- data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
- data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
- data/lib/swarm_memory/dsl/memory_config.rb +113 -0
- data/lib/swarm_memory/embeddings/embedder.rb +36 -0
- data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
- data/lib/swarm_memory/errors.rb +21 -0
- data/lib/swarm_memory/integration/cli_registration.rb +30 -0
- data/lib/swarm_memory/integration/configuration.rb +43 -0
- data/lib/swarm_memory/integration/registration.rb +31 -0
- data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
- data/lib/swarm_memory/optimization/analyzer.rb +244 -0
- data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
- data/lib/swarm_memory/prompts/memory.md.erb +109 -0
- data/lib/swarm_memory/prompts/memory_assistant.md.erb +181 -0
- data/lib/swarm_memory/prompts/memory_researcher.md.erb +281 -0
- data/lib/swarm_memory/prompts/memory_retrieval.md.erb +78 -0
- data/lib/swarm_memory/search/semantic_search.rb +112 -0
- data/lib/swarm_memory/search/text_search.rb +42 -0
- data/lib/swarm_memory/search/text_similarity.rb +80 -0
- data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
- data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
- data/lib/swarm_memory/tools/load_skill.rb +313 -0
- data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
- data/lib/swarm_memory/tools/memory_delete.rb +99 -0
- data/lib/swarm_memory/tools/memory_edit.rb +185 -0
- data/lib/swarm_memory/tools/memory_glob.rb +160 -0
- data/lib/swarm_memory/tools/memory_grep.rb +247 -0
- data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
- data/lib/swarm_memory/tools/memory_read.rb +123 -0
- data/lib/swarm_memory/tools/memory_write.rb +231 -0
- data/lib/swarm_memory/utils.rb +50 -0
- data/lib/swarm_memory/version.rb +5 -0
- data/lib/swarm_memory.rb +166 -0
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
- data/lib/swarm_sdk/agent/builder.rb +461 -0
- data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
- data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
- data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
- data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
- data/lib/swarm_sdk/agent/chat.rb +1159 -0
- data/lib/swarm_sdk/agent/context.rb +112 -0
- data/lib/swarm_sdk/agent/context_manager.rb +309 -0
- data/lib/swarm_sdk/agent/definition.rb +556 -0
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
- data/lib/swarm_sdk/configuration.rb +296 -0
- data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
- data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
- data/lib/swarm_sdk/context_compactor.rb +340 -0
- data/lib/swarm_sdk/hooks/adapter.rb +359 -0
- data/lib/swarm_sdk/hooks/context.rb +197 -0
- data/lib/swarm_sdk/hooks/definition.rb +80 -0
- data/lib/swarm_sdk/hooks/error.rb +29 -0
- data/lib/swarm_sdk/hooks/executor.rb +146 -0
- data/lib/swarm_sdk/hooks/registry.rb +147 -0
- data/lib/swarm_sdk/hooks/result.rb +150 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
- data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
- data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
- data/lib/swarm_sdk/log_collector.rb +51 -0
- data/lib/swarm_sdk/log_stream.rb +69 -0
- data/lib/swarm_sdk/markdown_parser.rb +75 -0
- data/lib/swarm_sdk/model_aliases.json +5 -0
- data/lib/swarm_sdk/models.json +1 -0
- data/lib/swarm_sdk/models.rb +120 -0
- data/lib/swarm_sdk/node/agent_config.rb +49 -0
- data/lib/swarm_sdk/node/builder.rb +439 -0
- data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
- data/lib/swarm_sdk/node_context.rb +170 -0
- data/lib/swarm_sdk/node_orchestrator.rb +384 -0
- data/lib/swarm_sdk/permissions/config.rb +239 -0
- data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
- data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
- data/lib/swarm_sdk/permissions/validator.rb +173 -0
- data/lib/swarm_sdk/permissions_builder.rb +122 -0
- data/lib/swarm_sdk/plugin.rb +147 -0
- data/lib/swarm_sdk/plugin_registry.rb +101 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
- data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
- data/lib/swarm_sdk/result.rb +97 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
- data/lib/swarm_sdk/swarm/builder.rb +586 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +419 -0
- data/lib/swarm_sdk/swarm.rb +982 -0
- data/lib/swarm_sdk/tools/bash.rb +274 -0
- data/lib/swarm_sdk/tools/clock.rb +44 -0
- data/lib/swarm_sdk/tools/delegate.rb +164 -0
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
- data/lib/swarm_sdk/tools/edit.rb +150 -0
- data/lib/swarm_sdk/tools/glob.rb +158 -0
- data/lib/swarm_sdk/tools/grep.rb +228 -0
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
- data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
- data/lib/swarm_sdk/tools/read.rb +251 -0
- data/lib/swarm_sdk/tools/registry.rb +93 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
- data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
- data/lib/swarm_sdk/tools/think.rb +95 -0
- data/lib/swarm_sdk/tools/todo_write.rb +216 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
- data/lib/swarm_sdk/tools/write.rb +117 -0
- data/lib/swarm_sdk/utils.rb +50 -0
- data/lib/swarm_sdk/version.rb +5 -0
- data/lib/swarm_sdk.rb +157 -0
- data/llm.v2.txt +13407 -0
- data/rubocop/cop/security/no_reflection_methods.rb +47 -0
- data/rubocop/cop/security/no_ruby_llm_logger.rb +32 -0
- data/swarm_cli.gemspec +57 -0
- data/swarm_memory.gemspec +28 -0
- data/swarm_sdk.gemspec +41 -0
- data/team.yml +1 -1
- data/team_full.yml +1875 -0
- data/{team_v2.yml → team_sdk.yml} +121 -52
- metadata +247 -4
- data/EXAMPLES.md +0 -164
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Node
|
|
5
|
+
# AgentConfig provides fluent API for configuring agents within a node
|
|
6
|
+
#
|
|
7
|
+
# This class enables the chainable syntax:
|
|
8
|
+
# agent(:backend).delegates_to(:tester, :database)
|
|
9
|
+
#
|
|
10
|
+
# @example Basic delegation
|
|
11
|
+
# agent(:backend).delegates_to(:tester)
|
|
12
|
+
#
|
|
13
|
+
# @example No delegation (solo agent)
|
|
14
|
+
# agent(:planner)
|
|
15
|
+
class AgentConfig
|
|
16
|
+
attr_reader :agent_name
|
|
17
|
+
|
|
18
|
+
def initialize(agent_name, node_builder)
|
|
19
|
+
@agent_name = agent_name
|
|
20
|
+
@node_builder = node_builder
|
|
21
|
+
@delegates_to = []
|
|
22
|
+
@finalized = false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Set delegation targets for this agent
|
|
26
|
+
#
|
|
27
|
+
# @param agent_names [Array<Symbol>] Names of agents to delegate to
|
|
28
|
+
# @return [self] For method chaining
|
|
29
|
+
def delegates_to(*agent_names)
|
|
30
|
+
@delegates_to = agent_names.map(&:to_sym)
|
|
31
|
+
finalize
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Finalize agent configuration (called automatically)
|
|
36
|
+
#
|
|
37
|
+
# Registers this agent configuration with the parent node builder.
|
|
38
|
+
# If delegates_to was never called, registers with empty delegation.
|
|
39
|
+
#
|
|
40
|
+
# @return [void]
|
|
41
|
+
def finalize
|
|
42
|
+
return if @finalized
|
|
43
|
+
|
|
44
|
+
@node_builder.register_agent(@agent_name, @delegates_to)
|
|
45
|
+
@finalized = true
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
module Node
|
|
5
|
+
# Builder provides DSL for configuring nodes (mini-swarms within a workflow)
|
|
6
|
+
#
|
|
7
|
+
# A node represents a stage in a multi-step workflow where a specific set
|
|
8
|
+
# of agents collaborate. Each node creates an independent swarm execution.
|
|
9
|
+
#
|
|
10
|
+
# @example Solo agent node
|
|
11
|
+
# node :planning do
|
|
12
|
+
# agent(:architect)
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# @example Multi-agent node with delegation
|
|
16
|
+
# node :implementation do
|
|
17
|
+
# agent(:backend).delegates_to(:tester, :database)
|
|
18
|
+
# agent(:tester).delegates_to(:database)
|
|
19
|
+
# agent(:database)
|
|
20
|
+
#
|
|
21
|
+
# depends_on :planning
|
|
22
|
+
# end
|
|
23
|
+
class Builder
|
|
24
|
+
attr_reader :name,
|
|
25
|
+
:agent_configs,
|
|
26
|
+
:dependencies,
|
|
27
|
+
:lead_override,
|
|
28
|
+
:input_transformer,
|
|
29
|
+
:output_transformer,
|
|
30
|
+
:input_transformer_command,
|
|
31
|
+
:output_transformer_command
|
|
32
|
+
|
|
33
|
+
def initialize(name)
|
|
34
|
+
@name = name
|
|
35
|
+
@agent_configs = []
|
|
36
|
+
@dependencies = []
|
|
37
|
+
@lead_override = nil
|
|
38
|
+
@input_transformer = nil # Ruby block
|
|
39
|
+
@output_transformer = nil # Ruby block
|
|
40
|
+
@input_transformer_command = nil # Bash command
|
|
41
|
+
@output_transformer_command = nil # Bash command
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Configure an agent for this node
|
|
45
|
+
#
|
|
46
|
+
# Returns an AgentConfig object that supports fluent delegation syntax.
|
|
47
|
+
# If delegates_to is not called, the agent is registered with no delegation.
|
|
48
|
+
#
|
|
49
|
+
# @param name [Symbol] Agent name
|
|
50
|
+
# @return [AgentConfig] Fluent configuration object
|
|
51
|
+
#
|
|
52
|
+
# @example With delegation
|
|
53
|
+
# agent(:backend).delegates_to(:tester, :database)
|
|
54
|
+
#
|
|
55
|
+
# @example Without delegation
|
|
56
|
+
# agent(:planner)
|
|
57
|
+
def agent(name)
|
|
58
|
+
config = AgentConfig.new(name, self)
|
|
59
|
+
|
|
60
|
+
# Register immediately with empty delegation
|
|
61
|
+
# If delegates_to is called later, it will update this
|
|
62
|
+
register_agent(name, [])
|
|
63
|
+
|
|
64
|
+
config
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Register an agent configuration (called by AgentConfig)
|
|
68
|
+
#
|
|
69
|
+
# @param agent_name [Symbol] Agent name
|
|
70
|
+
# @param delegates_to [Array<Symbol>] Delegation targets
|
|
71
|
+
# @return [void]
|
|
72
|
+
def register_agent(agent_name, delegates_to)
|
|
73
|
+
# Check if agent already registered
|
|
74
|
+
existing = @agent_configs.find { |ac| ac[:agent] == agent_name }
|
|
75
|
+
|
|
76
|
+
if existing
|
|
77
|
+
# Update delegation (happens when delegates_to is called after agent())
|
|
78
|
+
existing[:delegates_to] = delegates_to
|
|
79
|
+
else
|
|
80
|
+
# Add new agent configuration
|
|
81
|
+
@agent_configs << { agent: agent_name, delegates_to: delegates_to }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Declare dependencies (nodes that must execute before this one)
|
|
86
|
+
#
|
|
87
|
+
# @param node_names [Array<Symbol>] Names of prerequisite nodes
|
|
88
|
+
# @return [void]
|
|
89
|
+
#
|
|
90
|
+
# @example Single dependency
|
|
91
|
+
# depends_on :planning
|
|
92
|
+
#
|
|
93
|
+
# @example Multiple dependencies
|
|
94
|
+
# depends_on :frontend, :backend
|
|
95
|
+
def depends_on(*node_names)
|
|
96
|
+
@dependencies.concat(node_names.map(&:to_sym))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Override the lead agent (first agent is lead by default)
|
|
100
|
+
#
|
|
101
|
+
# @param agent_name [Symbol] Name of agent to make lead
|
|
102
|
+
# @return [void]
|
|
103
|
+
#
|
|
104
|
+
# @example
|
|
105
|
+
# agent(:backend).delegates_to(:tester)
|
|
106
|
+
# agent(:tester)
|
|
107
|
+
# lead :tester # tester is lead instead of backend
|
|
108
|
+
def lead(agent_name)
|
|
109
|
+
@lead_override = agent_name.to_sym
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Define input transformer for this node
|
|
113
|
+
#
|
|
114
|
+
# The transformer receives a NodeContext object with access to:
|
|
115
|
+
# - Previous node's result (convenience: ctx.content)
|
|
116
|
+
# - Original user prompt (ctx.original_prompt)
|
|
117
|
+
# - All previous node results (ctx.all_results[:node_name])
|
|
118
|
+
# - Current node metadata (ctx.node_name, ctx.dependencies)
|
|
119
|
+
#
|
|
120
|
+
# Can also be used for side effects (logging, file I/O) since the block
|
|
121
|
+
# runs at execution time, not declaration time.
|
|
122
|
+
#
|
|
123
|
+
# **Skip Execution**: Return a hash with `skip_execution: true` to skip
|
|
124
|
+
# the node's swarm execution and immediately return the provided content.
|
|
125
|
+
# Useful for caching, validation, or conditional execution.
|
|
126
|
+
#
|
|
127
|
+
# @yield [NodeContext] Context with previous results and metadata
|
|
128
|
+
# @return [String, Hash] Transformed input OR skip hash
|
|
129
|
+
#
|
|
130
|
+
# @example Access previous result and original prompt
|
|
131
|
+
# input do |ctx|
|
|
132
|
+
# # Convenience accessor
|
|
133
|
+
# previous_content = ctx.content
|
|
134
|
+
#
|
|
135
|
+
# # Access original prompt
|
|
136
|
+
# "Original: #{ctx.original_prompt}\nPrevious: #{previous_content}"
|
|
137
|
+
# end
|
|
138
|
+
#
|
|
139
|
+
# @example Access results from specific nodes
|
|
140
|
+
# input do |ctx|
|
|
141
|
+
# plan = ctx.all_results[:planning].content
|
|
142
|
+
# design = ctx.all_results[:design].content
|
|
143
|
+
#
|
|
144
|
+
# "Implement based on:\nPlan: #{plan}\nDesign: #{design}"
|
|
145
|
+
# end
|
|
146
|
+
#
|
|
147
|
+
# @example Skip execution (caching)
|
|
148
|
+
# input do |ctx|
|
|
149
|
+
# cached = check_cache(ctx.content)
|
|
150
|
+
# if cached
|
|
151
|
+
# # Skip LLM call, return cached result
|
|
152
|
+
# { skip_execution: true, content: cached }
|
|
153
|
+
# else
|
|
154
|
+
# ctx.content
|
|
155
|
+
# end
|
|
156
|
+
# end
|
|
157
|
+
#
|
|
158
|
+
# @example Skip execution (validation)
|
|
159
|
+
# input do |ctx|
|
|
160
|
+
# if ctx.content.length > 10000
|
|
161
|
+
# # Fail early without LLM call
|
|
162
|
+
# { skip_execution: true, content: "ERROR: Input too long" }
|
|
163
|
+
# else
|
|
164
|
+
# ctx.content
|
|
165
|
+
# end
|
|
166
|
+
# end
|
|
167
|
+
def input(&block)
|
|
168
|
+
@input_transformer = block
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Set input transformer as bash command (YAML API)
|
|
172
|
+
#
|
|
173
|
+
# The command receives NodeContext as JSON on STDIN and outputs transformed content.
|
|
174
|
+
#
|
|
175
|
+
# **Exit codes:**
|
|
176
|
+
# - 0: Success, use STDOUT as transformed content
|
|
177
|
+
# - 1: Skip node execution, use current_input unchanged (STDOUT ignored)
|
|
178
|
+
# - 2: Halt workflow with error, show STDERR (STDOUT ignored)
|
|
179
|
+
#
|
|
180
|
+
# @param command [String] Bash command to execute
|
|
181
|
+
# @param timeout [Integer] Timeout in seconds (default: 60)
|
|
182
|
+
# @return [void]
|
|
183
|
+
#
|
|
184
|
+
# @example
|
|
185
|
+
# input_command("scripts/validate.sh", timeout: 30)
|
|
186
|
+
def input_command(command, timeout: TransformerExecutor::DEFAULT_TIMEOUT)
|
|
187
|
+
@input_transformer_command = { command: command, timeout: timeout }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Define output transformer for this node
|
|
191
|
+
#
|
|
192
|
+
# The transformer receives a NodeContext object with access to:
|
|
193
|
+
# - Current node's result (convenience: ctx.content)
|
|
194
|
+
# - Original user prompt (ctx.original_prompt)
|
|
195
|
+
# - All completed node results (ctx.all_results[:node_name])
|
|
196
|
+
# - Current node metadata (ctx.node_name)
|
|
197
|
+
#
|
|
198
|
+
# Can also be used for side effects (logging, file I/O) since the block
|
|
199
|
+
# runs at execution time, not declaration time.
|
|
200
|
+
#
|
|
201
|
+
# @yield [NodeContext] Context with current result and metadata
|
|
202
|
+
# @return [String] Transformed output
|
|
203
|
+
#
|
|
204
|
+
# @example Transform and save to file
|
|
205
|
+
# output do |ctx|
|
|
206
|
+
# # Side effect: save to file
|
|
207
|
+
# File.write("results/plan.txt", ctx.content)
|
|
208
|
+
#
|
|
209
|
+
# # Return transformed output for next node
|
|
210
|
+
# "Key decisions: #{extract_decisions(ctx.content)}"
|
|
211
|
+
# end
|
|
212
|
+
#
|
|
213
|
+
# @example Access original prompt
|
|
214
|
+
# output do |ctx|
|
|
215
|
+
# # Include original context in output
|
|
216
|
+
# "Task: #{ctx.original_prompt}\nResult: #{ctx.content}"
|
|
217
|
+
# end
|
|
218
|
+
#
|
|
219
|
+
# @example Access multiple node results
|
|
220
|
+
# output do |ctx|
|
|
221
|
+
# plan = ctx.all_results[:planning].content
|
|
222
|
+
# impl = ctx.content
|
|
223
|
+
#
|
|
224
|
+
# "Completed:\nPlan: #{plan}\nImpl: #{impl}"
|
|
225
|
+
# end
|
|
226
|
+
def output(&block)
|
|
227
|
+
@output_transformer = block
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Set output transformer as bash command (YAML API)
|
|
231
|
+
#
|
|
232
|
+
# The command receives NodeContext as JSON on STDIN and outputs transformed content.
|
|
233
|
+
#
|
|
234
|
+
# **Exit codes:**
|
|
235
|
+
# - 0: Success, use STDOUT as transformed content
|
|
236
|
+
# - 1: Pass through unchanged, use result.content (STDOUT ignored)
|
|
237
|
+
# - 2: Halt workflow with error, show STDERR (STDOUT ignored)
|
|
238
|
+
#
|
|
239
|
+
# @param command [String] Bash command to execute
|
|
240
|
+
# @param timeout [Integer] Timeout in seconds (default: 60)
|
|
241
|
+
# @return [void]
|
|
242
|
+
#
|
|
243
|
+
# @example
|
|
244
|
+
# output_command("scripts/format.sh", timeout: 30)
|
|
245
|
+
def output_command(command, timeout: TransformerExecutor::DEFAULT_TIMEOUT)
|
|
246
|
+
@output_transformer_command = { command: command, timeout: timeout }
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Check if node has any input transformer (block or command)
|
|
250
|
+
#
|
|
251
|
+
# @return [Boolean]
|
|
252
|
+
def has_input_transformer?
|
|
253
|
+
@input_transformer || @input_transformer_command
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Check if node has any output transformer (block or command)
|
|
257
|
+
#
|
|
258
|
+
# @return [Boolean]
|
|
259
|
+
def has_output_transformer?
|
|
260
|
+
@output_transformer || @output_transformer_command
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Transform input using configured transformer (block or command)
|
|
264
|
+
#
|
|
265
|
+
# Executes either Ruby block or bash command transformer.
|
|
266
|
+
#
|
|
267
|
+
# **Exit code behavior (bash commands only):**
|
|
268
|
+
# - Exit 0: Use STDOUT as transformed content
|
|
269
|
+
# - Exit 1: Skip node execution, use current_input unchanged (STDOUT ignored)
|
|
270
|
+
# - Exit 2: Halt workflow with error (STDOUT ignored)
|
|
271
|
+
#
|
|
272
|
+
# @param context [NodeContext] Context with previous results and metadata
|
|
273
|
+
# @param current_input [String] Fallback content for exit 1 (skip), also used for halt error context
|
|
274
|
+
# @return [String, Hash] Transformed input OR skip hash `{ skip_execution: true, content: "..." }`
|
|
275
|
+
# @raise [ConfigurationError] If bash transformer halts workflow (exit 2)
|
|
276
|
+
def transform_input(context, current_input:)
|
|
277
|
+
# No transformer configured: return content as-is
|
|
278
|
+
return context.content unless @input_transformer || @input_transformer_command
|
|
279
|
+
|
|
280
|
+
# Ruby block transformer
|
|
281
|
+
# Ruby blocks can return String (transformed content) OR Hash (skip_execution)
|
|
282
|
+
if @input_transformer
|
|
283
|
+
return @input_transformer.call(context)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Bash command transformer
|
|
287
|
+
# Bash commands use exit codes to control behavior:
|
|
288
|
+
# - Exit 0: Success, use STDOUT as transformed content
|
|
289
|
+
# - Exit 1: Skip node execution, use current_input unchanged (STDOUT ignored)
|
|
290
|
+
# - Exit 2: Halt workflow with error (STDOUT ignored)
|
|
291
|
+
if @input_transformer_command
|
|
292
|
+
result = TransformerExecutor.execute(
|
|
293
|
+
command: @input_transformer_command[:command],
|
|
294
|
+
context: context,
|
|
295
|
+
event: "input",
|
|
296
|
+
node_name: @name,
|
|
297
|
+
fallback_content: current_input, # Used for exit 1 (skip)
|
|
298
|
+
timeout: @input_transformer_command[:timeout],
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Handle transformer result based on exit code
|
|
302
|
+
if result.halt?
|
|
303
|
+
# Exit 2: Halt workflow with error
|
|
304
|
+
raise ConfigurationError,
|
|
305
|
+
"Input transformer halted workflow for node '#{@name}': #{result.error_message}"
|
|
306
|
+
elsif result.skip_execution?
|
|
307
|
+
# Exit 1: Skip node execution, return skip hash
|
|
308
|
+
# Content is current_input unchanged (STDOUT was ignored)
|
|
309
|
+
{ skip_execution: true, content: result.content }
|
|
310
|
+
else
|
|
311
|
+
# Exit 0: Return transformed content from STDOUT
|
|
312
|
+
result.content
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Transform output using configured transformer (block or command)
|
|
318
|
+
#
|
|
319
|
+
# Executes either Ruby block or bash command transformer.
|
|
320
|
+
#
|
|
321
|
+
# **Exit code behavior (bash commands only):**
|
|
322
|
+
# - Exit 0: Use STDOUT as transformed content
|
|
323
|
+
# - Exit 1: Pass through unchanged, use result.content (STDOUT ignored)
|
|
324
|
+
# - Exit 2: Halt workflow with error (STDOUT ignored)
|
|
325
|
+
#
|
|
326
|
+
# @param context [NodeContext] Context with current result and metadata
|
|
327
|
+
# @return [String] Transformed output
|
|
328
|
+
# @raise [ConfigurationError] If bash transformer halts workflow (exit 2)
|
|
329
|
+
def transform_output(context)
|
|
330
|
+
# No transformer configured: return content as-is
|
|
331
|
+
return context.content unless @output_transformer || @output_transformer_command
|
|
332
|
+
|
|
333
|
+
# Ruby block transformer
|
|
334
|
+
# Simply calls the block with context and returns result
|
|
335
|
+
if @output_transformer
|
|
336
|
+
return @output_transformer.call(context)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Bash command transformer
|
|
340
|
+
# Bash commands use exit codes to control behavior:
|
|
341
|
+
# - Exit 0: Success, use STDOUT as transformed content
|
|
342
|
+
# - Exit 1: Pass through unchanged, use result.content (STDOUT ignored)
|
|
343
|
+
# - Exit 2: Halt workflow with error from STDERR (STDOUT ignored)
|
|
344
|
+
if @output_transformer_command
|
|
345
|
+
result = TransformerExecutor.execute(
|
|
346
|
+
command: @output_transformer_command[:command],
|
|
347
|
+
context: context,
|
|
348
|
+
event: "output",
|
|
349
|
+
node_name: @name,
|
|
350
|
+
fallback_content: context.content, # result.content for exit 1
|
|
351
|
+
timeout: @output_transformer_command[:timeout],
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Handle transformer result based on exit code
|
|
355
|
+
if result.halt?
|
|
356
|
+
# Exit 2: Halt workflow with error
|
|
357
|
+
raise ConfigurationError,
|
|
358
|
+
"Output transformer halted workflow for node '#{@name}': #{result.error_message}"
|
|
359
|
+
else
|
|
360
|
+
# Exit 0: Return transformed content from STDOUT
|
|
361
|
+
# Exit 1: Return fallback (result.content unchanged)
|
|
362
|
+
result.content
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# Get the lead agent for this node
|
|
368
|
+
#
|
|
369
|
+
# @return [Symbol] Lead agent name
|
|
370
|
+
def lead_agent
|
|
371
|
+
@lead_override || @agent_configs.first&.dig(:agent)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Check if this is an agent-less (computation-only) node
|
|
375
|
+
#
|
|
376
|
+
# Agent-less nodes run pure Ruby code without LLM execution.
|
|
377
|
+
# They must have at least one transformer (input or output).
|
|
378
|
+
#
|
|
379
|
+
# @return [Boolean]
|
|
380
|
+
def agent_less?
|
|
381
|
+
@agent_configs.empty?
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# Validate node configuration
|
|
385
|
+
#
|
|
386
|
+
# Also auto-adds agents that are referenced in delegates_to but not explicitly declared.
|
|
387
|
+
# This allows writing: agent(:backend).delegates_to(:verifier)
|
|
388
|
+
# without needing: agent(:verifier)
|
|
389
|
+
#
|
|
390
|
+
# @return [void]
|
|
391
|
+
# @raise [ConfigurationError] If configuration is invalid
|
|
392
|
+
def validate!
|
|
393
|
+
# Auto-add agents mentioned in delegates_to but not explicitly declared
|
|
394
|
+
auto_add_delegate_agents
|
|
395
|
+
|
|
396
|
+
# Agent-less nodes (pure computation) are allowed but need transformers
|
|
397
|
+
if @agent_configs.empty?
|
|
398
|
+
unless has_input_transformer? || has_output_transformer?
|
|
399
|
+
raise ConfigurationError,
|
|
400
|
+
"Agent-less node '#{@name}' must have at least one transformer (input or output). " \
|
|
401
|
+
"Either add agents with agent(:name) or add input/output transformers."
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# If has agents, validate lead override
|
|
406
|
+
if @lead_override && !@agent_configs.any? { |ac| ac[:agent] == @lead_override }
|
|
407
|
+
raise ConfigurationError,
|
|
408
|
+
"Node '#{@name}' lead agent '#{@lead_override}' not found in node's agents"
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
private
|
|
413
|
+
|
|
414
|
+
# Auto-add agents that are mentioned in delegates_to but not explicitly declared
|
|
415
|
+
#
|
|
416
|
+
# This allows:
|
|
417
|
+
# agent(:backend).delegates_to(:tester)
|
|
418
|
+
# Without needing:
|
|
419
|
+
# agent(:tester)
|
|
420
|
+
#
|
|
421
|
+
# The tester agent is automatically added to the node with no delegation.
|
|
422
|
+
#
|
|
423
|
+
# @return [void]
|
|
424
|
+
def auto_add_delegate_agents
|
|
425
|
+
# Collect all agents mentioned in delegates_to
|
|
426
|
+
all_delegates = @agent_configs.flat_map { |ac| ac[:delegates_to] }.uniq
|
|
427
|
+
|
|
428
|
+
# Find delegates that aren't explicitly declared
|
|
429
|
+
declared_agents = @agent_configs.map { |ac| ac[:agent] }
|
|
430
|
+
missing_delegates = all_delegates - declared_agents
|
|
431
|
+
|
|
432
|
+
# Auto-add missing delegates with empty delegation
|
|
433
|
+
missing_delegates.each do |delegate_name|
|
|
434
|
+
@agent_configs << { agent: delegate_name, delegates_to: [] }
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|