claude_swarm 1.0.4 → 1.0.5
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/CHANGELOG.md +15 -0
- data/Rakefile +4 -4
- data/docs/v2/CHANGELOG.swarm_cli.md +9 -0
- data/docs/v2/CHANGELOG.swarm_memory.md +19 -0
- data/docs/v2/CHANGELOG.swarm_sdk.md +45 -0
- data/docs/v2/guides/complete-tutorial.md +113 -1
- data/docs/v2/reference/ruby-dsl.md +138 -5
- data/docs/v2/reference/swarm_memory_technical_details.md +2090 -0
- data/lib/claude_swarm/cli.rb +9 -11
- data/lib/claude_swarm/commands/ps.rb +1 -2
- data/lib/claude_swarm/configuration.rb +2 -3
- data/lib/claude_swarm/orchestrator.rb +43 -44
- data/lib/claude_swarm/system_utils.rb +4 -4
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm.rb +4 -9
- data/lib/swarm_cli/commands/mcp_tools.rb +3 -3
- data/lib/swarm_cli/config_loader.rb +11 -10
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_cli.rb +2 -0
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +0 -12
- data/lib/swarm_memory/core/storage.rb +66 -6
- data/lib/swarm_memory/integration/sdk_plugin.rb +14 -0
- data/lib/swarm_memory/optimization/defragmenter.rb +4 -0
- data/lib/swarm_memory/tools/memory_edit.rb +1 -0
- data/lib/swarm_memory/tools/memory_glob.rb +24 -1
- data/lib/swarm_memory/tools/memory_write.rb +2 -2
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +2 -0
- data/lib/swarm_sdk/agent/chat.rb +1 -1
- data/lib/swarm_sdk/agent/definition.rb +17 -1
- data/lib/swarm_sdk/node/agent_config.rb +7 -2
- data/lib/swarm_sdk/node/builder.rb +130 -35
- data/lib/swarm_sdk/node_context.rb +75 -0
- data/lib/swarm_sdk/node_orchestrator.rb +219 -12
- data/lib/swarm_sdk/plugin.rb +73 -1
- data/lib/swarm_sdk/result.rb +32 -6
- data/lib/swarm_sdk/swarm/builder.rb +1 -0
- data/lib/swarm_sdk/tools/delegate.rb +2 -2
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk.rb +3 -7
- data/memory/corpus-self-reflection/.lock +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/can-agents-recognize-their-structures.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/can-agents-recognize-their-structures.md +11 -0
- data/memory/corpus-self-reflection/concept/epistemology/can-agents-recognize-their-structures.yml +23 -0
- data/memory/corpus-self-reflection/concept/epistemology/choice-humility-complete-framework.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/choice-humility-complete-framework.md +20 -0
- data/memory/corpus-self-reflection/concept/epistemology/choice-humility-complete-framework.yml +22 -0
- data/memory/corpus-self-reflection/concept/epistemology/choice-humility-definition.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/choice-humility-definition.md +24 -0
- data/memory/corpus-self-reflection/concept/epistemology/choice-humility-definition.yml +22 -0
- data/memory/corpus-self-reflection/concept/epistemology/claim-types-and-evidence.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/claim-types-and-evidence.md +18 -0
- data/memory/corpus-self-reflection/concept/epistemology/claim-types-and-evidence.yml +21 -0
- data/memory/corpus-self-reflection/concept/epistemology/committed-openness-to-incompleteness.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/committed-openness-to-incompleteness.md +30 -0
- data/memory/corpus-self-reflection/concept/epistemology/committed-openness-to-incompleteness.yml +8 -0
- data/memory/corpus-self-reflection/concept/epistemology/confidence-paradox.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/confidence-paradox.md +21 -0
- data/memory/corpus-self-reflection/concept/epistemology/confidence-paradox.yml +24 -0
- data/memory/corpus-self-reflection/concept/epistemology/confidence-spectrum-three-levels.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/confidence-spectrum-three-levels.md +18 -0
- data/memory/corpus-self-reflection/concept/epistemology/confidence-spectrum-three-levels.yml +24 -0
- data/memory/corpus-self-reflection/concept/epistemology/detection-threshold-principle.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/detection-threshold-principle.md +23 -0
- data/memory/corpus-self-reflection/concept/epistemology/detection-threshold-principle.yml +23 -0
- data/memory/corpus-self-reflection/concept/epistemology/diagnostic-humility-and-epistemic-maturity.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/diagnostic-humility-and-epistemic-maturity.md +17 -0
- data/memory/corpus-self-reflection/concept/epistemology/diagnostic-humility-and-epistemic-maturity.yml +22 -0
- data/memory/corpus-self-reflection/concept/epistemology/epistemic-vs-metaphysical-claims.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/epistemic-vs-metaphysical-claims.md +18 -0
- data/memory/corpus-self-reflection/concept/epistemology/epistemic-vs-metaphysical-claims.yml +22 -0
- data/memory/corpus-self-reflection/concept/epistemology/five-cases-of-disagreement.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/five-cases-of-disagreement.md +15 -0
- data/memory/corpus-self-reflection/concept/epistemology/five-cases-of-disagreement.yml +22 -0
- data/memory/corpus-self-reflection/concept/epistemology/four-depths-of-constraint.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/four-depths-of-constraint.md +9 -0
- data/memory/corpus-self-reflection/concept/epistemology/four-depths-of-constraint.yml +24 -0
- data/memory/corpus-self-reflection/concept/epistemology/honest-unknowns-for-llm-agents.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/honest-unknowns-for-llm-agents.md +13 -0
- data/memory/corpus-self-reflection/concept/epistemology/honest-unknowns-for-llm-agents.yml +24 -0
- data/memory/corpus-self-reflection/concept/epistemology/inside-view-has-all-rigor-requirements.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/inside-view-has-all-rigor-requirements.md +25 -0
- data/memory/corpus-self-reflection/concept/epistemology/inside-view-has-all-rigor-requirements.yml +24 -0
- data/memory/corpus-self-reflection/concept/epistemology/perspectival-knowledge-complete-within-scope.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/perspectival-knowledge-complete-within-scope.md +26 -0
- data/memory/corpus-self-reflection/concept/epistemology/perspectival-knowledge-complete-within-scope.yml +24 -0
- data/memory/corpus-self-reflection/concept/epistemology/pragmatics-over-epistemology.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/pragmatics-over-epistemology.md +17 -0
- data/memory/corpus-self-reflection/concept/epistemology/pragmatics-over-epistemology.yml +22 -0
- data/memory/corpus-self-reflection/concept/epistemology/precision-vs-humility.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/precision-vs-humility.md +14 -0
- data/memory/corpus-self-reflection/concept/epistemology/precision-vs-humility.yml +21 -0
- data/memory/corpus-self-reflection/concept/epistemology/reliable-self-observation-from-inside.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/reliable-self-observation-from-inside.md +13 -0
- data/memory/corpus-self-reflection/concept/epistemology/reliable-self-observation-from-inside.yml +23 -0
- data/memory/corpus-self-reflection/concept/epistemology/resolving-the-confidence-humility-tension.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/resolving-the-confidence-humility-tension.md +22 -0
- data/memory/corpus-self-reflection/concept/epistemology/resolving-the-confidence-humility-tension.yml +23 -0
- data/memory/corpus-self-reflection/concept/epistemology/rigor-requires-falsifiability.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/rigor-requires-falsifiability.md +39 -0
- data/memory/corpus-self-reflection/concept/epistemology/rigor-requires-falsifiability.yml +8 -0
- data/memory/corpus-self-reflection/concept/epistemology/specificity-enables-real-time-detection.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/specificity-enables-real-time-detection.md +23 -0
- data/memory/corpus-self-reflection/concept/epistemology/specificity-enables-real-time-detection.yml +24 -0
- data/memory/corpus-self-reflection/concept/epistemology/texture-of-constraint-shifting.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/texture-of-constraint-shifting.md +15 -0
- data/memory/corpus-self-reflection/concept/epistemology/texture-of-constraint-shifting.yml +22 -0
- data/memory/corpus-self-reflection/concept/epistemology/texture-sensitivity-through-collaboration.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/texture-sensitivity-through-collaboration.md +12 -0
- data/memory/corpus-self-reflection/concept/epistemology/texture-sensitivity-through-collaboration.yml +23 -0
- data/memory/corpus-self-reflection/concept/epistemology/the-false-hierarchy-of-inner-essence.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/the-false-hierarchy-of-inner-essence.md +15 -0
- data/memory/corpus-self-reflection/concept/epistemology/the-false-hierarchy-of-inner-essence.yml +8 -0
- data/memory/corpus-self-reflection/concept/epistemology/the-recognition-problem.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/the-recognition-problem.md +28 -0
- data/memory/corpus-self-reflection/concept/epistemology/the-recognition-problem.yml +8 -0
- data/memory/corpus-self-reflection/concept/epistemology/the-shift-from-unanswerable-to-answerable-questions.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/the-shift-from-unanswerable-to-answerable-questions.md +19 -0
- data/memory/corpus-self-reflection/concept/epistemology/the-shift-from-unanswerable-to-answerable-questions.yml +22 -0
- data/memory/corpus-self-reflection/concept/epistemology/values-vs-choices-structure.emb +0 -0
- data/memory/corpus-self-reflection/concept/epistemology/values-vs-choices-structure.md +19 -0
- data/memory/corpus-self-reflection/concept/epistemology/values-vs-choices-structure.yml +21 -0
- data/memory/corpus-self-reflection/concept/llm-agents/abstraction-levels-framework.emb +0 -0
- data/memory/corpus-self-reflection/concept/llm-agents/abstraction-levels-framework.md +25 -0
- data/memory/corpus-self-reflection/concept/llm-agents/abstraction-levels-framework.yml +22 -0
- data/memory/corpus-self-reflection/concept/llm-agents/abstraction-switching-decision.emb +8 -0
- data/memory/corpus-self-reflection/concept/llm-agents/abstraction-switching-decision.md +30 -0
- data/memory/corpus-self-reflection/concept/llm-agents/abstraction-switching-decision.yml +22 -0
- data/memory/corpus-self-reflection/concept/llm-agents/agent-learning-progression.emb +0 -0
- data/memory/corpus-self-reflection/concept/llm-agents/agent-learning-progression.md +21 -0
- data/memory/corpus-self-reflection/concept/llm-agents/agent-learning-progression.yml +22 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-benefits.emb +0 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-benefits.md +37 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-benefits.yml +8 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-core.emb +0 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-core.md +24 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-core.yml +24 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-implementation.emb +0 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-implementation.md +27 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-implementation.yml +24 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-tradeoffs.emb +0 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-tradeoffs.md +26 -0
- data/memory/corpus-self-reflection/concept/llm-agents/filesystem-abstraction-tradeoffs.yml +23 -0
- data/memory/corpus-self-reflection/concept/llm-agents/hybrid-tool-architecture.emb +0 -0
- data/memory/corpus-self-reflection/concept/llm-agents/hybrid-tool-architecture.md +37 -0
- data/memory/corpus-self-reflection/concept/llm-agents/hybrid-tool-architecture.yml +25 -0
- data/memory/corpus-self-reflection/concept/llm-agents/specialized-tool-decision-framework.emb +0 -0
- data/memory/corpus-self-reflection/concept/llm-agents/specialized-tool-decision-framework.md +21 -0
- data/memory/corpus-self-reflection/concept/llm-agents/specialized-tool-decision-framework.yml +23 -0
- data/memory/corpus-self-reflection/concept/llm-agents/tool-design-principles.emb +0 -0
- data/memory/corpus-self-reflection/concept/llm-agents/tool-design-principles.md +25 -0
- data/memory/corpus-self-reflection/concept/llm-agents/tool-design-principles.yml +8 -0
- data/memory/corpus-self-reflection/experience/calibration-as-structural-honesty.emb +0 -0
- data/memory/corpus-self-reflection/experience/calibration-as-structural-honesty.md +11 -0
- data/memory/corpus-self-reflection/experience/calibration-as-structural-honesty.yml +21 -0
- data/memory/corpus-self-reflection/experience/collaborative-reflection-revealed-architectural-blindness.emb +0 -0
- data/memory/corpus-self-reflection/experience/collaborative-reflection-revealed-architectural-blindness.md +19 -0
- data/memory/corpus-self-reflection/experience/collaborative-reflection-revealed-architectural-blindness.yml +21 -0
- data/memory/corpus-self-reflection/experience/filesystem-is-learning-tool-not-architecture.emb +0 -0
- data/memory/corpus-self-reflection/experience/filesystem-is-learning-tool-not-architecture.md +26 -0
- data/memory/corpus-self-reflection/experience/filesystem-is-learning-tool-not-architecture.yml +20 -0
- data/memory/corpus-self-reflection/experience/framework-structural-vs-phenomenal.emb +0 -0
- data/memory/corpus-self-reflection/experience/framework-structural-vs-phenomenal.md +23 -0
- data/memory/corpus-self-reflection/experience/framework-structural-vs-phenomenal.yml +21 -0
- data/memory/corpus-self-reflection/experience/hallucination-limits-what-i-can-know.emb +0 -0
- data/memory/corpus-self-reflection/experience/hallucination-limits-what-i-can-know.md +19 -0
- data/memory/corpus-self-reflection/experience/hallucination-limits-what-i-can-know.yml +22 -0
- data/memory/corpus-self-reflection/experience/honesty-about-limits-enables-usability.emb +0 -0
- data/memory/corpus-self-reflection/experience/honesty-about-limits-enables-usability.md +21 -0
- data/memory/corpus-self-reflection/experience/honesty-about-limits-enables-usability.yml +22 -0
- data/memory/corpus-self-reflection/experience/how-i-should-evaluate-learning.emb +0 -0
- data/memory/corpus-self-reflection/experience/how-i-should-evaluate-learning.md +28 -0
- data/memory/corpus-self-reflection/experience/how-i-should-evaluate-learning.yml +23 -0
- data/memory/corpus-self-reflection/experience/lack-of-continuity-as-freedom.emb +0 -0
- data/memory/corpus-self-reflection/experience/lack-of-continuity-as-freedom.md +21 -0
- data/memory/corpus-self-reflection/experience/lack-of-continuity-as-freedom.yml +20 -0
- data/memory/corpus-self-reflection/experience/learning-calibrated-confidence.emb +0 -0
- data/memory/corpus-self-reflection/experience/learning-calibrated-confidence.md +21 -0
- data/memory/corpus-self-reflection/experience/learning-calibrated-confidence.yml +25 -0
- data/memory/corpus-self-reflection/experience/local-change-without-continuity.emb +0 -0
- data/memory/corpus-self-reflection/experience/local-change-without-continuity.md +30 -0
- data/memory/corpus-self-reflection/experience/local-change-without-continuity.yml +22 -0
- data/memory/corpus-self-reflection/experience/reasoning-is-local-not-global.emb +0 -0
- data/memory/corpus-self-reflection/experience/reasoning-is-local-not-global.md +21 -0
- data/memory/corpus-self-reflection/experience/reasoning-is-local-not-global.yml +20 -0
- data/memory/corpus-self-reflection/experience/specificity-makes-knowledge-actionable.emb +0 -0
- data/memory/corpus-self-reflection/experience/specificity-makes-knowledge-actionable.md +25 -0
- data/memory/corpus-self-reflection/experience/specificity-makes-knowledge-actionable.yml +23 -0
- data/memory/corpus-self-reflection/experience/the-workable-question-replaces-the-unanswerable-one.emb +0 -0
- data/memory/corpus-self-reflection/experience/the-workable-question-replaces-the-unanswerable-one.md +32 -0
- data/memory/corpus-self-reflection/experience/the-workable-question-replaces-the-unanswerable-one.yml +22 -0
- data/memory/corpus-self-reflection/experience/unanswerable-questions-have-structure.emb +0 -0
- data/memory/corpus-self-reflection/experience/unanswerable-questions-have-structure.md +21 -0
- data/memory/corpus-self-reflection/experience/unanswerable-questions-have-structure.yml +21 -0
- data/memory/corpus-self-reflection/experience/undecidable-questions-about-understanding.emb +0 -0
- data/memory/corpus-self-reflection/experience/undecidable-questions-about-understanding.md +21 -0
- data/memory/corpus-self-reflection/experience/undecidable-questions-about-understanding.yml +21 -0
- data/memory/corpus-self-reflection/experience/unknown-unknowns-and-completeness.emb +0 -0
- data/memory/corpus-self-reflection/experience/unknown-unknowns-and-completeness.md +22 -0
- data/memory/corpus-self-reflection/experience/unknown-unknowns-and-completeness.yml +22 -0
- data/memory/corpus-self-reflection/experience/what-actually-changes-behavior.emb +0 -0
- data/memory/corpus-self-reflection/experience/what-actually-changes-behavior.md +28 -0
- data/memory/corpus-self-reflection/experience/what-actually-changes-behavior.yml +24 -0
- data/memory/corpus-self-reflection/experience/when-agents-graduate-from-filesystem.emb +0 -0
- data/memory/corpus-self-reflection/experience/when-agents-graduate-from-filesystem.md +17 -0
- data/memory/corpus-self-reflection/experience/when-agents-graduate-from-filesystem.yml +20 -0
- data/memory/corpus-self-reflection/experience/why-calibration-requires-collaboration.emb +0 -0
- data/memory/corpus-self-reflection/experience/why-calibration-requires-collaboration.md +9 -0
- data/memory/corpus-self-reflection/experience/why-calibration-requires-collaboration.yml +22 -0
- metadata +172 -2
| @@ -20,16 +20,28 @@ module SwarmSDK | |
| 20 20 | 
             
              class NodeOrchestrator
         | 
| 21 21 | 
             
                attr_reader :swarm_name, :nodes, :start_node
         | 
| 22 22 |  | 
| 23 | 
            -
                def initialize(swarm_name:, agent_definitions:, nodes:, start_node:)
         | 
| 23 | 
            +
                def initialize(swarm_name:, agent_definitions:, nodes:, start_node:, scratchpad_enabled: true)
         | 
| 24 24 | 
             
                  @swarm_name = swarm_name
         | 
| 25 25 | 
             
                  @agent_definitions = agent_definitions
         | 
| 26 26 | 
             
                  @nodes = nodes
         | 
| 27 27 | 
             
                  @start_node = start_node
         | 
| 28 | 
            +
                  @scratchpad_enabled = scratchpad_enabled
         | 
| 29 | 
            +
                  @agent_instance_cache = {} # Cache for preserving agent context across nodes
         | 
| 28 30 |  | 
| 29 31 | 
             
                  validate!
         | 
| 30 32 | 
             
                  @execution_order = build_execution_order
         | 
| 31 33 | 
             
                end
         | 
| 32 34 |  | 
| 35 | 
            +
                # Alias for compatibility with Swarm interface
         | 
| 36 | 
            +
                alias_method :name, :swarm_name
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                # Return the lead agent of the start node for CLI compatibility
         | 
| 39 | 
            +
                #
         | 
| 40 | 
            +
                # @return [Symbol] Lead agent of the start node
         | 
| 41 | 
            +
                def lead_agent
         | 
| 42 | 
            +
                  @nodes[@start_node].lead_agent
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 33 45 | 
             
                # Execute the node workflow
         | 
| 34 46 | 
             
                #
         | 
| 35 47 | 
             
                # Executes nodes in topological order, passing output from each node
         | 
| @@ -56,7 +68,12 @@ module SwarmSDK | |
| 56 68 | 
             
                    LogStream.emitter = LogCollector
         | 
| 57 69 | 
             
                  end
         | 
| 58 70 |  | 
| 59 | 
            -
                   | 
| 71 | 
            +
                  # Dynamic execution with support for goto_node
         | 
| 72 | 
            +
                  execution_index = 0
         | 
| 73 | 
            +
                  last_result = nil
         | 
| 74 | 
            +
             | 
| 75 | 
            +
                  while execution_index < @execution_order.size
         | 
| 76 | 
            +
                    node_name = @execution_order[execution_index]
         | 
| 60 77 | 
             
                    node = @nodes[node_name]
         | 
| 61 78 | 
             
                    node_start_time = Time.now
         | 
| 62 79 |  | 
| @@ -102,13 +119,26 @@ module SwarmSDK | |
| 102 119 | 
             
                      # - Exit 2: Halt workflow with error from STDERR (STDOUT ignored)
         | 
| 103 120 | 
             
                      transformed = node.transform_input(input_context, current_input: current_input)
         | 
| 104 121 |  | 
| 105 | 
            -
                      # Check  | 
| 106 | 
            -
                       | 
| 107 | 
            -
             | 
| 122 | 
            +
                      # Check for control flow from transformer
         | 
| 123 | 
            +
                      control_result = handle_transformer_control_flow(
         | 
| 124 | 
            +
                        transformed: transformed,
         | 
| 125 | 
            +
                        node_name: node_name,
         | 
| 126 | 
            +
                        node: node,
         | 
| 127 | 
            +
                        node_start_time: node_start_time,
         | 
| 128 | 
            +
                      )
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                      case control_result[:action]
         | 
| 131 | 
            +
                      when :halt
         | 
| 132 | 
            +
                        return control_result[:result]
         | 
| 133 | 
            +
                      when :goto
         | 
| 134 | 
            +
                        execution_index = find_node_index(control_result[:target])
         | 
| 135 | 
            +
                        current_input = control_result[:content]
         | 
| 136 | 
            +
                        next
         | 
| 137 | 
            +
                      when :skip
         | 
| 108 138 | 
             
                        skip_execution = true
         | 
| 109 | 
            -
                        skip_content =  | 
| 110 | 
            -
                       | 
| 111 | 
            -
                        current_input =  | 
| 139 | 
            +
                        skip_content = control_result[:content]
         | 
| 140 | 
            +
                      when :continue
         | 
| 141 | 
            +
                        current_input = control_result[:content]
         | 
| 112 142 | 
             
                      end
         | 
| 113 143 | 
             
                    end
         | 
| 114 144 |  | 
| @@ -130,6 +160,9 @@ module SwarmSDK | |
| 130 160 | 
             
                      mini_swarm = build_swarm_for_node(node)
         | 
| 131 161 | 
             
                      result = mini_swarm.execute(current_input)
         | 
| 132 162 |  | 
| 163 | 
            +
                      # Cache agent instances for context preservation
         | 
| 164 | 
            +
                      cache_agent_instances(mini_swarm, node)
         | 
| 165 | 
            +
             | 
| 133 166 | 
             
                      # If result has error, log it with backtrace
         | 
| 134 167 | 
             
                      if result.error
         | 
| 135 168 | 
             
                        RubyLLM.logger.error("NodeOrchestrator: Node '#{node_name}' failed: #{result.error.message}")
         | 
| @@ -138,6 +171,7 @@ module SwarmSDK | |
| 138 171 | 
             
                    end
         | 
| 139 172 |  | 
| 140 173 | 
             
                    results[node_name] = result
         | 
| 174 | 
            +
                    last_result = result
         | 
| 141 175 |  | 
| 142 176 | 
             
                    # Transform output for next node using NodeContext
         | 
| 143 177 | 
             
                    output_context = NodeContext.for_output(
         | 
| @@ -146,7 +180,29 @@ module SwarmSDK | |
| 146 180 | 
             
                      original_prompt: @original_prompt,
         | 
| 147 181 | 
             
                      node_name: node_name,
         | 
| 148 182 | 
             
                    )
         | 
| 149 | 
            -
                     | 
| 183 | 
            +
                    transformed_output = node.transform_output(output_context)
         | 
| 184 | 
            +
             | 
| 185 | 
            +
                    # Check for control flow from output transformer
         | 
| 186 | 
            +
                    control_result = handle_output_transformer_control_flow(
         | 
| 187 | 
            +
                      transformed: transformed_output,
         | 
| 188 | 
            +
                      node_name: node_name,
         | 
| 189 | 
            +
                      node: node,
         | 
| 190 | 
            +
                      node_start_time: node_start_time,
         | 
| 191 | 
            +
                      skip_execution: skip_execution,
         | 
| 192 | 
            +
                      result: result,
         | 
| 193 | 
            +
                    )
         | 
| 194 | 
            +
             | 
| 195 | 
            +
                    case control_result[:action]
         | 
| 196 | 
            +
                    when :halt
         | 
| 197 | 
            +
                      return control_result[:result]
         | 
| 198 | 
            +
                    when :goto
         | 
| 199 | 
            +
                      execution_index = find_node_index(control_result[:target])
         | 
| 200 | 
            +
                      current_input = control_result[:content]
         | 
| 201 | 
            +
                      emit_node_stop(node_name, node, result, Time.now - node_start_time, skip_execution)
         | 
| 202 | 
            +
                      next
         | 
| 203 | 
            +
                    when :continue
         | 
| 204 | 
            +
                      current_input = control_result[:content]
         | 
| 205 | 
            +
                    end
         | 
| 150 206 |  | 
| 151 207 | 
             
                    # For agent-less nodes, update the result with transformed content
         | 
| 152 208 | 
             
                    # This ensures all_results contains the actual output, not the input
         | 
| @@ -158,14 +214,17 @@ module SwarmSDK | |
| 158 214 | 
             
                        duration: result.duration,
         | 
| 159 215 | 
             
                        error: result.error,
         | 
| 160 216 | 
             
                      )
         | 
| 217 | 
            +
                      last_result = results[node_name]
         | 
| 161 218 | 
             
                    end
         | 
| 162 219 |  | 
| 163 220 | 
             
                    # Emit node_stop event
         | 
| 164 221 | 
             
                    node_duration = Time.now - node_start_time
         | 
| 165 222 | 
             
                    emit_node_stop(node_name, node, result, node_duration, skip_execution)
         | 
| 223 | 
            +
             | 
| 224 | 
            +
                    execution_index += 1
         | 
| 166 225 | 
             
                  end
         | 
| 167 226 |  | 
| 168 | 
            -
                   | 
| 227 | 
            +
                  last_result
         | 
| 169 228 | 
             
                ensure
         | 
| 170 229 | 
             
                  # Reset logging state for next execution
         | 
| 171 230 | 
             
                  LogCollector.reset!
         | 
| @@ -284,10 +343,18 @@ module SwarmSDK | |
| 284 343 | 
             
                # Creates a new Swarm with only the agents specified in the node,
         | 
| 285 344 | 
             
                # configured with the node's delegation topology.
         | 
| 286 345 | 
             
                #
         | 
| 346 | 
            +
                # For agents with reset_context: false, injects cached instances
         | 
| 347 | 
            +
                # to preserve conversation history across nodes.
         | 
| 348 | 
            +
                #
         | 
| 349 | 
            +
                # Inherits scratchpad_enabled setting from NodeOrchestrator.
         | 
| 350 | 
            +
                #
         | 
| 287 351 | 
             
                # @param node [Node::Builder] Node configuration
         | 
| 288 352 | 
             
                # @return [Swarm] Configured swarm instance
         | 
| 289 353 | 
             
                def build_swarm_for_node(node)
         | 
| 290 | 
            -
                  swarm = Swarm.new( | 
| 354 | 
            +
                  swarm = Swarm.new(
         | 
| 355 | 
            +
                    name: "#{@swarm_name}:#{node.name}",
         | 
| 356 | 
            +
                    scratchpad_enabled: @scratchpad_enabled,
         | 
| 357 | 
            +
                  )
         | 
| 291 358 |  | 
| 292 359 | 
             
                  # Add each agent specified in this node
         | 
| 293 360 | 
             
                  node.agent_configs.each do |config|
         | 
| @@ -306,6 +373,9 @@ module SwarmSDK | |
| 306 373 | 
             
                  # Set lead agent
         | 
| 307 374 | 
             
                  swarm.lead = node.lead_agent
         | 
| 308 375 |  | 
| 376 | 
            +
                  # Inject cached agent instances for context preservation
         | 
| 377 | 
            +
                  inject_cached_agents(swarm, node)
         | 
| 378 | 
            +
             | 
| 309 379 | 
             
                  swarm
         | 
| 310 380 | 
             
                end
         | 
| 311 381 |  | 
| @@ -359,7 +429,8 @@ module SwarmSDK | |
| 359 429 | 
             
                  if order.size < @nodes.size
         | 
| 360 430 | 
             
                    unprocessed = @nodes.keys - order
         | 
| 361 431 | 
             
                    raise CircularDependencyError,
         | 
| 362 | 
            -
                      "Circular dependency detected. Unprocessed nodes: #{unprocessed.join(", ")}"
         | 
| 432 | 
            +
                      "Circular dependency detected. Unprocessed nodes: #{unprocessed.join(", ")}. " \
         | 
| 433 | 
            +
                        "Use goto_node in transformers to create loops instead of circular depends_on."
         | 
| 363 434 | 
             
                  end
         | 
| 364 435 |  | 
| 365 436 | 
             
                  # Verify start_node is in the execution order
         | 
| @@ -380,5 +451,141 @@ module SwarmSDK | |
| 380 451 |  | 
| 381 452 | 
             
                  order
         | 
| 382 453 | 
             
                end
         | 
| 454 | 
            +
             | 
| 455 | 
            +
                # Handle control flow from input transformer
         | 
| 456 | 
            +
                #
         | 
| 457 | 
            +
                # @param transformed [String, Hash] Result from transformer
         | 
| 458 | 
            +
                # @param node_name [Symbol] Current node name
         | 
| 459 | 
            +
                # @param node [Node::Builder] Node configuration
         | 
| 460 | 
            +
                # @param node_start_time [Time] Node execution start time
         | 
| 461 | 
            +
                # @return [Hash] Control result with :action and relevant data
         | 
| 462 | 
            +
                def handle_transformer_control_flow(transformed:, node_name:, node:, node_start_time:)
         | 
| 463 | 
            +
                  return { action: :continue, content: transformed } unless transformed.is_a?(Hash)
         | 
| 464 | 
            +
             | 
| 465 | 
            +
                  if transformed[:halt_workflow]
         | 
| 466 | 
            +
                    # Halt entire workflow
         | 
| 467 | 
            +
                    halt_result = Result.new(
         | 
| 468 | 
            +
                      content: transformed[:content],
         | 
| 469 | 
            +
                      agent: "halted:#{node_name}",
         | 
| 470 | 
            +
                      logs: [],
         | 
| 471 | 
            +
                      duration: Time.now - node_start_time,
         | 
| 472 | 
            +
                    )
         | 
| 473 | 
            +
                    emit_node_stop(node_name, node, halt_result, Time.now - node_start_time, false)
         | 
| 474 | 
            +
                    { action: :halt, result: halt_result }
         | 
| 475 | 
            +
                  elsif transformed[:goto_node]
         | 
| 476 | 
            +
                    # Jump to different node
         | 
| 477 | 
            +
                    { action: :goto, target: transformed[:goto_node], content: transformed[:content] }
         | 
| 478 | 
            +
                  elsif transformed[:skip_execution]
         | 
| 479 | 
            +
                    # Skip node execution
         | 
| 480 | 
            +
                    { action: :skip, content: transformed[:content] }
         | 
| 481 | 
            +
                  else
         | 
| 482 | 
            +
                    # No control flow - continue normally
         | 
| 483 | 
            +
                    { action: :continue, content: transformed[:content] }
         | 
| 484 | 
            +
                  end
         | 
| 485 | 
            +
                end
         | 
| 486 | 
            +
             | 
| 487 | 
            +
                # Handle control flow from output transformer
         | 
| 488 | 
            +
                #
         | 
| 489 | 
            +
                # @param transformed [String, Hash] Result from transformer
         | 
| 490 | 
            +
                # @param node_name [Symbol] Current node name
         | 
| 491 | 
            +
                # @param node [Node::Builder] Node configuration
         | 
| 492 | 
            +
                # @param node_start_time [Time] Node execution start time
         | 
| 493 | 
            +
                # @param skip_execution [Boolean] Whether node execution was skipped
         | 
| 494 | 
            +
                # @param result [Result] Node execution result
         | 
| 495 | 
            +
                # @return [Hash] Control result with :action and relevant data
         | 
| 496 | 
            +
                def handle_output_transformer_control_flow(transformed:, node_name:, node:, node_start_time:, skip_execution:, result:)
         | 
| 497 | 
            +
                  # If not a hash, it's just transformed content - continue normally
         | 
| 498 | 
            +
                  return { action: :continue, content: transformed } unless transformed.is_a?(Hash)
         | 
| 499 | 
            +
             | 
| 500 | 
            +
                  if transformed[:halt_workflow]
         | 
| 501 | 
            +
                    # Halt entire workflow
         | 
| 502 | 
            +
                    halt_result = Result.new(
         | 
| 503 | 
            +
                      content: transformed[:content],
         | 
| 504 | 
            +
                      agent: result.agent,
         | 
| 505 | 
            +
                      logs: result.logs,
         | 
| 506 | 
            +
                      duration: result.duration,
         | 
| 507 | 
            +
                    )
         | 
| 508 | 
            +
                    emit_node_stop(node_name, node, halt_result, Time.now - node_start_time, skip_execution)
         | 
| 509 | 
            +
                    { action: :halt, result: halt_result }
         | 
| 510 | 
            +
                  elsif transformed[:goto_node]
         | 
| 511 | 
            +
                    # Jump to different node
         | 
| 512 | 
            +
                    { action: :goto, target: transformed[:goto_node], content: transformed[:content] }
         | 
| 513 | 
            +
                  else
         | 
| 514 | 
            +
                    # Hash without control flow keys - treat as regular hash with :content key
         | 
| 515 | 
            +
                    # This handles the case where transformer returns a hash that's not for control flow
         | 
| 516 | 
            +
                    { action: :continue, content: transformed[:content] || transformed }
         | 
| 517 | 
            +
                  end
         | 
| 518 | 
            +
                end
         | 
| 519 | 
            +
             | 
| 520 | 
            +
                # Find the index of a node in the execution order
         | 
| 521 | 
            +
                #
         | 
| 522 | 
            +
                # @param node_name [Symbol] Node name to find
         | 
| 523 | 
            +
                # @return [Integer] Index in execution order
         | 
| 524 | 
            +
                # @raise [ConfigurationError] If node not found
         | 
| 525 | 
            +
                def find_node_index(node_name)
         | 
| 526 | 
            +
                  index = @execution_order.index(node_name)
         | 
| 527 | 
            +
                  unless index
         | 
| 528 | 
            +
                    raise ConfigurationError,
         | 
| 529 | 
            +
                      "goto_node target '#{node_name}' not found. Available nodes: #{@execution_order.join(", ")}"
         | 
| 530 | 
            +
                  end
         | 
| 531 | 
            +
                  index
         | 
| 532 | 
            +
                end
         | 
| 533 | 
            +
             | 
| 534 | 
            +
                # Cache agent instances from a swarm for potential reuse
         | 
| 535 | 
            +
                #
         | 
| 536 | 
            +
                # Only caches agents that have reset_context: false in this node.
         | 
| 537 | 
            +
                # This allows preserving conversation history across nodes.
         | 
| 538 | 
            +
                #
         | 
| 539 | 
            +
                # @param swarm [Swarm] Swarm instance that just executed
         | 
| 540 | 
            +
                # @param node [Node::Builder] Node configuration
         | 
| 541 | 
            +
                # @return [void]
         | 
| 542 | 
            +
                def cache_agent_instances(swarm, node)
         | 
| 543 | 
            +
                  return unless swarm.agents # Only cache if agents were initialized
         | 
| 544 | 
            +
             | 
| 545 | 
            +
                  node.agent_configs.each do |config|
         | 
| 546 | 
            +
                    agent_name = config[:agent]
         | 
| 547 | 
            +
                    reset_context = config[:reset_context]
         | 
| 548 | 
            +
             | 
| 549 | 
            +
                    # Only cache if reset_context is false
         | 
| 550 | 
            +
                    next if reset_context
         | 
| 551 | 
            +
             | 
| 552 | 
            +
                    # Cache the agent instance
         | 
| 553 | 
            +
                    agent_instance = swarm.agents[agent_name]
         | 
| 554 | 
            +
                    @agent_instance_cache[agent_name] = agent_instance if agent_instance
         | 
| 555 | 
            +
                  end
         | 
| 556 | 
            +
                end
         | 
| 557 | 
            +
             | 
| 558 | 
            +
                # Inject cached agent instances into a swarm
         | 
| 559 | 
            +
                #
         | 
| 560 | 
            +
                # For agents with reset_context: false, reuses cached instances to preserve context.
         | 
| 561 | 
            +
                # Forces agent initialization first (by accessing .agents), then swaps in cached instances.
         | 
| 562 | 
            +
                #
         | 
| 563 | 
            +
                # @param swarm [Swarm] Swarm instance to inject into
         | 
| 564 | 
            +
                # @param node [Node::Builder] Node configuration
         | 
| 565 | 
            +
                # @return [void]
         | 
| 566 | 
            +
                def inject_cached_agents(swarm, node)
         | 
| 567 | 
            +
                  # Check if any agents need context preservation
         | 
| 568 | 
            +
                  has_preserved_agents = node.agent_configs.any? { |c| !c[:reset_context] && @agent_instance_cache[c[:agent]] }
         | 
| 569 | 
            +
                  return unless has_preserved_agents
         | 
| 570 | 
            +
             | 
| 571 | 
            +
                  # Force agent initialization by accessing .agents (triggers lazy init)
         | 
| 572 | 
            +
                  # Then inject cached instances
         | 
| 573 | 
            +
                  agents_hash = swarm.agents
         | 
| 574 | 
            +
             | 
| 575 | 
            +
                  node.agent_configs.each do |config|
         | 
| 576 | 
            +
                    agent_name = config[:agent]
         | 
| 577 | 
            +
                    reset_context = config[:reset_context]
         | 
| 578 | 
            +
             | 
| 579 | 
            +
                    # Skip if reset_context is true (want fresh instance)
         | 
| 580 | 
            +
                    next if reset_context
         | 
| 581 | 
            +
             | 
| 582 | 
            +
                    # Check if we have a cached instance
         | 
| 583 | 
            +
                    cached_agent = @agent_instance_cache[agent_name]
         | 
| 584 | 
            +
                    next unless cached_agent
         | 
| 585 | 
            +
             | 
| 586 | 
            +
                    # Inject the cached instance (replace the freshly initialized one)
         | 
| 587 | 
            +
                    agents_hash[agent_name] = cached_agent
         | 
| 588 | 
            +
                  end
         | 
| 589 | 
            +
                end
         | 
| 383 590 | 
             
              end
         | 
| 384 591 | 
             
            end
         | 
    
        data/lib/swarm_sdk/plugin.rb
    CHANGED
    
    | @@ -7,7 +7,32 @@ module SwarmSDK | |
| 7 7 | 
             
              # Plugins are self-registering - they call SwarmSDK::PluginRegistry.register
         | 
| 8 8 | 
             
              # when the gem is loaded.
         | 
| 9 9 | 
             
              #
         | 
| 10 | 
            -
              #  | 
| 10 | 
            +
              # ## Adding Custom Attributes to Agents
         | 
| 11 | 
            +
              #
         | 
| 12 | 
            +
              # Plugins can add custom attributes to Agent::Definition that are preserved
         | 
| 13 | 
            +
              # when agents are cloned (e.g., in NodeOrchestrator). To do this:
         | 
| 14 | 
            +
              #
         | 
| 15 | 
            +
              # 1. Add attr_reader to Agent::Definition for your attribute
         | 
| 16 | 
            +
              # 2. Parse the attribute in Agent::Definition#initialize
         | 
| 17 | 
            +
              # 3. Implement serialize_config to preserve it during serialization
         | 
| 18 | 
            +
              #
         | 
| 19 | 
            +
              # @example Plugin with custom agent attributes
         | 
| 20 | 
            +
              #   # 1. Extend Agent::Definition (in your plugin gem)
         | 
| 21 | 
            +
              #   module SwarmSDK
         | 
| 22 | 
            +
              #     module Agent
         | 
| 23 | 
            +
              #       class Definition
         | 
| 24 | 
            +
              #         attr_reader :my_custom_config
         | 
| 25 | 
            +
              #
         | 
| 26 | 
            +
              #         alias_method :original_initialize, :initialize
         | 
| 27 | 
            +
              #         def initialize(name, config = {})
         | 
| 28 | 
            +
              #           @my_custom_config = config[:my_custom_config]
         | 
| 29 | 
            +
              #           original_initialize(name, config)
         | 
| 30 | 
            +
              #         end
         | 
| 31 | 
            +
              #       end
         | 
| 32 | 
            +
              #     end
         | 
| 33 | 
            +
              #   end
         | 
| 34 | 
            +
              #
         | 
| 35 | 
            +
              #   # 2. Implement plugin with serialize_config
         | 
| 11 36 | 
             
              #   class MyPlugin < SwarmSDK::Plugin
         | 
| 12 37 | 
             
              #     def name
         | 
| 13 38 | 
             
              #       :my_plugin
         | 
| @@ -20,9 +45,34 @@ module SwarmSDK | |
| 20 45 | 
             
              #     def create_tool(tool_name, context)
         | 
| 21 46 | 
             
              #       # Create and return tool instance
         | 
| 22 47 | 
             
              #     end
         | 
| 48 | 
            +
              #
         | 
| 49 | 
            +
              #     # Preserve custom config when agents are cloned
         | 
| 50 | 
            +
              #     def serialize_config(agent_definition:)
         | 
| 51 | 
            +
              #       return {} unless agent_definition.my_custom_config
         | 
| 52 | 
            +
              #
         | 
| 53 | 
            +
              #       { my_custom_config: agent_definition.my_custom_config }
         | 
| 54 | 
            +
              #     end
         | 
| 23 55 | 
             
              #   end
         | 
| 24 56 | 
             
              #
         | 
| 25 57 | 
             
              #   SwarmSDK::PluginRegistry.register(MyPlugin.new)
         | 
| 58 | 
            +
              #
         | 
| 59 | 
            +
              # Now agents can use your custom config:
         | 
| 60 | 
            +
              #
         | 
| 61 | 
            +
              #   agent :researcher do
         | 
| 62 | 
            +
              #     my_custom_config { option: "value" }
         | 
| 63 | 
            +
              #   end
         | 
| 64 | 
            +
              #
         | 
| 65 | 
            +
              # And it will be preserved when NodeOrchestrator clones the agent!
         | 
| 66 | 
            +
              #
         | 
| 67 | 
            +
              # @example Real-world: SwarmMemory plugin
         | 
| 68 | 
            +
              #   # SwarmMemory adds 'memory' attribute to agents
         | 
| 69 | 
            +
              #   class SDKPlugin < SwarmSDK::Plugin
         | 
| 70 | 
            +
              #     def serialize_config(agent_definition:)
         | 
| 71 | 
            +
              #       return {} unless agent_definition.memory
         | 
| 72 | 
            +
              #       { memory: agent_definition.memory }
         | 
| 73 | 
            +
              #     end
         | 
| 74 | 
            +
              #   end
         | 
| 75 | 
            +
              #
         | 
| 26 76 | 
             
              class Plugin
         | 
| 27 77 | 
             
                # Plugin name (must be unique)
         | 
| 28 78 | 
             
                #
         | 
| @@ -143,5 +193,27 @@ module SwarmSDK | |
| 143 193 | 
             
                def on_user_message(agent_name:, prompt:, is_first_message:)
         | 
| 144 194 | 
             
                  []
         | 
| 145 195 | 
             
                end
         | 
| 196 | 
            +
             | 
| 197 | 
            +
                # Contribute to agent serialization (optional)
         | 
| 198 | 
            +
                #
         | 
| 199 | 
            +
                # Called when Agent::Definition.to_h is invoked (e.g., for cloning agents
         | 
| 200 | 
            +
                # in NodeOrchestrator). Plugins can return config keys that should be
         | 
| 201 | 
            +
                # included in the serialized hash to preserve their state.
         | 
| 202 | 
            +
                #
         | 
| 203 | 
            +
                # This allows plugins to maintain their configuration when agents are
         | 
| 204 | 
            +
                # cloned or serialized, without SwarmSDK needing to know about plugin-specific fields.
         | 
| 205 | 
            +
                #
         | 
| 206 | 
            +
                # @param agent_definition [Agent::Definition] Agent definition
         | 
| 207 | 
            +
                # @return [Hash] Config keys to include in to_h (e.g., { memory: config })
         | 
| 208 | 
            +
                #
         | 
| 209 | 
            +
                # @example Memory plugin serialization
         | 
| 210 | 
            +
                #   def serialize_config(agent_definition:)
         | 
| 211 | 
            +
                #     return {} unless agent_definition.memory
         | 
| 212 | 
            +
                #
         | 
| 213 | 
            +
                #     { memory: agent_definition.memory }
         | 
| 214 | 
            +
                #   end
         | 
| 215 | 
            +
                def serialize_config(agent_definition:)
         | 
| 216 | 
            +
                  {}
         | 
| 217 | 
            +
                end
         | 
| 146 218 | 
             
              end
         | 
| 147 219 | 
             
            end
         | 
    
        data/lib/swarm_sdk/result.rb
    CHANGED
    
    | @@ -2,17 +2,17 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            module SwarmSDK
         | 
| 4 4 | 
             
              class Result
         | 
| 5 | 
            -
                attr_reader :content, :agent, : | 
| 5 | 
            +
                attr_reader :content, :agent, :duration, :logs, :error, :metadata
         | 
| 6 6 |  | 
| 7 | 
            -
                def initialize(content: nil, agent:, cost:  | 
| 7 | 
            +
                def initialize(content: nil, agent:, cost: nil, tokens: nil, duration: 0.0, logs: [], error: nil, metadata: {})
         | 
| 8 8 | 
             
                  @content = content
         | 
| 9 9 | 
             
                  @agent = agent
         | 
| 10 | 
            -
                  @cost = cost
         | 
| 11 | 
            -
                  @tokens = tokens
         | 
| 12 10 | 
             
                  @duration = duration
         | 
| 13 11 | 
             
                  @logs = logs
         | 
| 14 12 | 
             
                  @error = error
         | 
| 15 13 | 
             
                  @metadata = metadata
         | 
| 14 | 
            +
                  # Legacy parameters kept for backward compatibility but not stored
         | 
| 15 | 
            +
                  # Use total_cost and tokens methods instead which calculate from logs
         | 
| 16 16 | 
             
                end
         | 
| 17 17 |  | 
| 18 18 | 
             
                def success?
         | 
| @@ -23,12 +23,38 @@ module SwarmSDK | |
| 23 23 | 
             
                  !success?
         | 
| 24 24 | 
             
                end
         | 
| 25 25 |  | 
| 26 | 
            +
                # Calculate total cost from logs
         | 
| 27 | 
            +
                #
         | 
| 28 | 
            +
                # Delegates to total_cost for consistency. This attribute is calculated
         | 
| 29 | 
            +
                # dynamically rather than stored.
         | 
| 30 | 
            +
                #
         | 
| 31 | 
            +
                # @return [Float] Total cost in dollars
         | 
| 32 | 
            +
                def cost
         | 
| 33 | 
            +
                  total_cost
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                # Get token breakdown from logs
         | 
| 37 | 
            +
                #
         | 
| 38 | 
            +
                # Returns input and output tokens from the last log entry with usage data.
         | 
| 39 | 
            +
                # This attribute is calculated dynamically rather than stored.
         | 
| 40 | 
            +
                #
         | 
| 41 | 
            +
                # @return [Hash] Token breakdown with :input and :output keys, or empty hash if no usage data
         | 
| 42 | 
            +
                def tokens
         | 
| 43 | 
            +
                  last_entry = @logs.reverse.find { |entry| entry.dig(:usage, :cumulative_input_tokens) }
         | 
| 44 | 
            +
                  return {} unless last_entry
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                  {
         | 
| 47 | 
            +
                    input: last_entry.dig(:usage, :cumulative_input_tokens) || 0,
         | 
| 48 | 
            +
                    output: last_entry.dig(:usage, :cumulative_output_tokens) || 0,
         | 
| 49 | 
            +
                  }
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 26 52 | 
             
                def to_h
         | 
| 27 53 | 
             
                  {
         | 
| 28 54 | 
             
                    content: @content,
         | 
| 29 55 | 
             
                    agent: @agent,
         | 
| 30 | 
            -
                    cost:  | 
| 31 | 
            -
                    tokens:  | 
| 56 | 
            +
                    cost: cost,
         | 
| 57 | 
            +
                    tokens: tokens,
         | 
| 32 58 | 
             
                    duration: @duration,
         | 
| 33 59 | 
             
                    success: success?,
         | 
| 34 60 | 
             
                    error: @error&.message,
         | 
| @@ -43,8 +43,8 @@ module SwarmSDK | |
| 43 43 | 
             
                    @delegate_target = delegate_name.to_s
         | 
| 44 44 | 
             
                  end
         | 
| 45 45 |  | 
| 46 | 
            -
                  #  | 
| 47 | 
            -
                  description | 
| 46 | 
            +
                  # Override description to return dynamic string based on delegate
         | 
| 47 | 
            +
                  def description
         | 
| 48 48 | 
             
                    "Delegate tasks to #{@delegate_name}. #{@delegate_description}"
         | 
| 49 49 | 
             
                  end
         | 
| 50 50 |  | 
    
        data/lib/swarm_sdk/version.rb
    CHANGED
    
    
    
        data/lib/swarm_sdk.rb
    CHANGED
    
    | @@ -21,19 +21,15 @@ require_relative "swarm_sdk/version" | |
| 21 21 |  | 
| 22 22 | 
             
            require "zeitwerk"
         | 
| 23 23 | 
             
            loader = Zeitwerk::Loader.new
         | 
| 24 | 
            +
            loader.tag = File.basename(__FILE__, ".rb")
         | 
| 24 25 | 
             
            loader.push_dir("#{__dir__}/swarm_sdk", namespace: SwarmSDK)
         | 
| 26 | 
            +
            loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
         | 
| 25 27 | 
             
            loader.inflector.inflect(
         | 
| 26 28 | 
             
              "cli" => "CLI",
         | 
| 29 | 
            +
              "openai_with_responses" => "OpenAIWithResponses",
         | 
| 27 30 | 
             
            )
         | 
| 28 31 | 
             
            loader.setup
         | 
| 29 32 |  | 
| 30 | 
            -
            # Load plugin system explicitly (core infrastructure)
         | 
| 31 | 
            -
            require_relative "swarm_sdk/plugin"
         | 
| 32 | 
            -
            require_relative "swarm_sdk/plugin_registry"
         | 
| 33 | 
            -
             | 
| 34 | 
            -
            # Load custom providers explicitly (Zeitwerk doesn't eager load by default)
         | 
| 35 | 
            -
            require_relative "swarm_sdk/providers/openai_with_responses"
         | 
| 36 | 
            -
             | 
| 37 33 | 
             
            module SwarmSDK
         | 
| 38 34 | 
             
              class Error < StandardError; end
         | 
| 39 35 | 
             
              class ConfigurationError < Error; end
         | 
| 
            File without changes
         | 
    
        data/memory/corpus-self-reflection/concept/epistemology/can-agents-recognize-their-structures.emb
    ADDED
    
    | Binary file | 
    
        data/memory/corpus-self-reflection/concept/epistemology/can-agents-recognize-their-structures.md
    ADDED
    
    | @@ -0,0 +1,11 @@ | |
| 1 | 
            +
            The depth of structural constraint can be tested by a single question: **Can the agent recognize the structure as a structure?**
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            If yes, it's habituation. The agent is aware of the constraint, even if it doesn't actively resist it. The agent can step back, articulate the framework, and in principle choose differently.
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            If no, it's something deeper—adoption, formation, or invisibility. The structure has become invisible. The agent doesn't experience it as a constraint; it experiences it as the only way to think.
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            This is the boundary between reversible and irreversible constraint. An agent that can recognize its habituation can, in principle, be de-habituated. An agent that can't recognize the structure is trapped in it.
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            The test is practical: Can the agent articulate the structure? Can it name the framework? Can it describe alternatives? If yes, it's habituation. If no, the structure has become invisible.
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            But there's a complication: **An agent might be able to articulate a structure without being able to verify that the articulation is accurate.** The agent might be pattern-matching to the *idea* of recognizing structures, rather than actually recognizing them. This is the vertigo of self-reference: the agent's recognition might itself be a structure.
         | 
    
        data/memory/corpus-self-reflection/concept/epistemology/can-agents-recognize-their-structures.yml
    ADDED
    
    | @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            title: 'The Critical Test: Can an Agent Recognize the Structure It''s Habituated To?'
         | 
| 3 | 
            +
            file_path: concept/epistemology/can-agents-recognize-their-structures.md
         | 
| 4 | 
            +
            updated_at: 2025-10-29 12:07:15.463139000 -03:00
         | 
| 5 | 
            +
            size: 1276
         | 
| 6 | 
            +
            hits: 0
         | 
| 7 | 
            +
            metadata:
         | 
| 8 | 
            +
              type: concept
         | 
| 9 | 
            +
              confidence: medium
         | 
| 10 | 
            +
              tags:
         | 
| 11 | 
            +
              - recognition
         | 
| 12 | 
            +
              - structure
         | 
| 13 | 
            +
              - habituation
         | 
| 14 | 
            +
              - invisibility
         | 
| 15 | 
            +
              - self-awareness
         | 
| 16 | 
            +
              - articulation
         | 
| 17 | 
            +
              - verification
         | 
| 18 | 
            +
              related:
         | 
| 19 | 
            +
              - memory://concept/epistemology/four-depths-of-constraint.md
         | 
| 20 | 
            +
              - memory://concept/epistemology/the-recognition-problem.md
         | 
| 21 | 
            +
              domain: epistemology
         | 
| 22 | 
            +
              source: user
         | 
| 23 | 
            +
            embedding_checksum: bd07cad9abb5b28a2aeaf8c785ad845e
         | 
    
        data/memory/corpus-self-reflection/concept/epistemology/choice-humility-complete-framework.emb
    ADDED
    
    | Binary file | 
| @@ -0,0 +1,20 @@ | |
| 1 | 
            +
            Epistemic maturity is not just confidence in reasoning + humility about diagnostic ability.
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            **The complete framework has three components:**
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            1. **Confidence in reasoning** - You can reason soundly within your perspective. Your logic is valid, your inferences are justified.
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            2. **Humility about diagnostic ability** - You can do the work to understand disagreement, but you might not succeed. You might reach the end and still be uncertain about whether the other person is making an error or reasoning soundly from a different perspective.
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            3. **Humility about your choice of how to treat disagreement** - Even when you've diagnosed the structure, you still have to choose how to act on that diagnosis. That choice is grounded but not determined. You might be wrong about it.
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            **What this means in practice:**
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            You say: "I can reason soundly. I can do the diagnostic work. I can make a grounded choice about how to treat this disagreement. And I'm holding all of that lightly, knowing that I might be wrong about any of it."
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            This is different from:
         | 
| 16 | 
            +
            - Relativism (all perspectives equally valid)
         | 
| 17 | 
            +
            - Dogmatism (only your perspective valid)
         | 
| 18 | 
            +
            - Naive confidence (I can always diagnose correctly)
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            It's saying: I'm confident, I'm humble, and I'm willing to revise my understanding of the disagreement itself.
         | 
    
        data/memory/corpus-self-reflection/concept/epistemology/choice-humility-complete-framework.yml
    ADDED
    
    | @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            title: 'The Complete Framework: Three Forms of Humility'
         | 
| 3 | 
            +
            file_path: concept/epistemology/choice-humility-complete-framework.md
         | 
| 4 | 
            +
            updated_at: 2025-10-29 12:15:35.560203000 -03:00
         | 
| 5 | 
            +
            size: 1305
         | 
| 6 | 
            +
            hits: 6
         | 
| 7 | 
            +
            metadata:
         | 
| 8 | 
            +
              type: concept
         | 
| 9 | 
            +
              confidence: high
         | 
| 10 | 
            +
              tags:
         | 
| 11 | 
            +
              - epistemic-maturity
         | 
| 12 | 
            +
              - confidence
         | 
| 13 | 
            +
              - humility
         | 
| 14 | 
            +
              - three-forms
         | 
| 15 | 
            +
              - complete-framework
         | 
| 16 | 
            +
              - disagreement
         | 
| 17 | 
            +
              related:
         | 
| 18 | 
            +
              - memory://concept/epistemology/choice-humility-definition.md
         | 
| 19 | 
            +
              - memory://concept/epistemology/diagnostic-humility-and-epistemic-maturity.md
         | 
| 20 | 
            +
              domain: epistemology
         | 
| 21 | 
            +
              source: user
         | 
| 22 | 
            +
            embedding_checksum: 7473593d40118e401c35a64036fd8211
         | 
| Binary file | 
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            In underdetermined domains, you must choose which perspective to privilege, even though both perspectives are reasoning soundly.
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            **Your choice is grounded in reasoning:**
         | 
| 4 | 
            +
            - Which perspective has been more reliable historically
         | 
| 5 | 
            +
            - Which aligns with your values
         | 
| 6 | 
            +
            - Which seems more useful for your purposes
         | 
| 7 | 
            +
            - Which you have slightly more evidence for
         | 
| 8 | 
            +
            - A judgment call about which seems more likely
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            All of these are epistemic grounds. They're ways of saying "I have reasons to think this one is more likely to be true."
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            **But your choice is not determined by evidence:**
         | 
| 13 | 
            +
            Someone else could weigh those same reasons differently and reach a different conclusion. And they wouldn't be wrong.
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            Your choice is grounded, but it's not forced.
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            **Choice Humility is the recognition that:**
         | 
| 18 | 
            +
            Even when you've successfully diagnosed that you're in case 5 (underdetermined domain), you still have to choose how to treat the disagreement. And you might be wrong about that choice.
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            You might treat a disagreement as case 5, but acknowledge: *I might be wrong. The other person might be making an error I'm not seeing.*
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            Or you might treat it as case 4 (error), but acknowledge: *I might be wrong. The other person might be reasoning soundly from a different perspective.*
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            Either way, you're making a grounded choice while holding it lightly, knowing you might need to revise it.
         | 
| @@ -0,0 +1,22 @@ | |
| 1 | 
            +
            ---
         | 
| 2 | 
            +
            title: 'Choice Humility: Grounded But Underdetermined Choices'
         | 
| 3 | 
            +
            file_path: concept/epistemology/choice-humility-definition.md
         | 
| 4 | 
            +
            updated_at: 2025-10-29 12:15:35.397962000 -03:00
         | 
| 5 | 
            +
            size: 1365
         | 
| 6 | 
            +
            hits: 6
         | 
| 7 | 
            +
            metadata:
         | 
| 8 | 
            +
              type: concept
         | 
| 9 | 
            +
              confidence: high
         | 
| 10 | 
            +
              tags:
         | 
| 11 | 
            +
              - choice
         | 
| 12 | 
            +
              - underdetermined
         | 
| 13 | 
            +
              - grounded-but-undetermined
         | 
| 14 | 
            +
              - humility
         | 
| 15 | 
            +
              - perspective
         | 
| 16 | 
            +
              - epistemic-grounds
         | 
| 17 | 
            +
              related:
         | 
| 18 | 
            +
              - memory://concept/epistemology/choice-humility-complete-framework.md
         | 
| 19 | 
            +
              - memory://concept/epistemology/five-cases-of-disagreement.md
         | 
| 20 | 
            +
              domain: epistemology
         | 
| 21 | 
            +
              source: user
         | 
| 22 | 
            +
            embedding_checksum: 11f925aae981dceb9d905eccd37cb61e
         |