swarm_sdk 2.1.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/agent/builder.rb +33 -0
  3. data/lib/swarm_sdk/agent/chat/context_tracker.rb +33 -0
  4. data/lib/swarm_sdk/agent/chat/hook_integration.rb +41 -0
  5. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +11 -27
  6. data/lib/swarm_sdk/agent/chat.rb +198 -51
  7. data/lib/swarm_sdk/agent/context.rb +6 -2
  8. data/lib/swarm_sdk/agent/context_manager.rb +6 -0
  9. data/lib/swarm_sdk/agent/definition.rb +15 -22
  10. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +180 -0
  11. data/lib/swarm_sdk/configuration.rb +420 -103
  12. data/lib/swarm_sdk/events_to_messages.rb +181 -0
  13. data/lib/swarm_sdk/log_collector.rb +31 -5
  14. data/lib/swarm_sdk/log_stream.rb +37 -8
  15. data/lib/swarm_sdk/model_aliases.json +4 -1
  16. data/lib/swarm_sdk/node/agent_config.rb +33 -8
  17. data/lib/swarm_sdk/node/builder.rb +39 -18
  18. data/lib/swarm_sdk/node_orchestrator.rb +293 -26
  19. data/lib/swarm_sdk/proc_helpers.rb +53 -0
  20. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
  21. data/lib/swarm_sdk/providers/openai_with_responses.rb +22 -15
  22. data/lib/swarm_sdk/restore_result.rb +65 -0
  23. data/lib/swarm_sdk/snapshot.rb +156 -0
  24. data/lib/swarm_sdk/snapshot_from_events.rb +386 -0
  25. data/lib/swarm_sdk/state_restorer.rb +491 -0
  26. data/lib/swarm_sdk/state_snapshot.rb +369 -0
  27. data/lib/swarm_sdk/swarm/agent_initializer.rb +360 -55
  28. data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
  29. data/lib/swarm_sdk/swarm/builder.rb +208 -12
  30. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
  31. data/lib/swarm_sdk/swarm/tool_configurator.rb +46 -11
  32. data/lib/swarm_sdk/swarm.rb +367 -90
  33. data/lib/swarm_sdk/swarm_loader.rb +145 -0
  34. data/lib/swarm_sdk/swarm_registry.rb +136 -0
  35. data/lib/swarm_sdk/tools/delegate.rb +92 -7
  36. data/lib/swarm_sdk/tools/read.rb +17 -5
  37. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
  38. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
  39. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
  40. data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
  41. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +45 -0
  42. data/lib/swarm_sdk/tools/stores/storage.rb +4 -4
  43. data/lib/swarm_sdk/tools/think.rb +4 -1
  44. data/lib/swarm_sdk/tools/todo_write.rb +20 -8
  45. data/lib/swarm_sdk/utils.rb +18 -0
  46. data/lib/swarm_sdk/validation_result.rb +33 -0
  47. data/lib/swarm_sdk/version.rb +1 -1
  48. data/lib/swarm_sdk.rb +362 -21
  49. metadata +17 -5
@@ -18,15 +18,39 @@ module SwarmSDK
18
18
  # )
19
19
  # result = orchestrator.execute("Build auth system")
20
20
  class NodeOrchestrator
21
- attr_reader :swarm_name, :nodes, :start_node
21
+ attr_reader :swarm_name, :nodes, :start_node, :agent_definitions, :agent_instance_cache, :scratchpad
22
+ attr_writer :swarm_id, :config_for_hooks
23
+ attr_accessor :swarm_registry_config
22
24
 
23
- def initialize(swarm_name:, agent_definitions:, nodes:, start_node:, scratchpad_enabled: true)
25
+ def initialize(swarm_name:, agent_definitions:, nodes:, start_node:, swarm_id: nil, scratchpad: :enabled, allow_filesystem_tools: nil)
24
26
  @swarm_name = swarm_name
27
+ @swarm_id = swarm_id
25
28
  @agent_definitions = agent_definitions
26
29
  @nodes = nodes
27
30
  @start_node = start_node
28
- @scratchpad_enabled = scratchpad_enabled
29
- @agent_instance_cache = {} # Cache for preserving agent context across nodes
31
+ @scratchpad = normalize_scratchpad_mode(scratchpad)
32
+ @allow_filesystem_tools = allow_filesystem_tools
33
+ @swarm_registry_config = [] # External swarms config (if using composable swarms)
34
+ @agent_instance_cache = {
35
+ primary: {}, # { agent_name => Agent::Chat }
36
+ delegations: {}, # { "delegate@delegator" => Agent::Chat }
37
+ }
38
+
39
+ # Initialize scratchpad storage based on mode
40
+ case @scratchpad
41
+ when :enabled
42
+ # Enabled mode: single scratchpad shared across all nodes
43
+ @shared_scratchpad_storage = Tools::Stores::ScratchpadStorage.new
44
+ @node_scratchpads = nil
45
+ when :per_node
46
+ # Per-node mode: separate scratchpad per node (lazy initialized)
47
+ @shared_scratchpad_storage = nil
48
+ @node_scratchpads = {}
49
+ when :disabled
50
+ # Disabled: no storage at all
51
+ @shared_scratchpad_storage = nil
52
+ @node_scratchpads = nil
53
+ end
30
54
 
31
55
  validate!
32
56
  @execution_order = build_execution_order
@@ -35,6 +59,72 @@ module SwarmSDK
35
59
  # Alias for compatibility with Swarm interface
36
60
  alias_method :name, :swarm_name
37
61
 
62
+ # Get scratchpad storage for a specific node
63
+ #
64
+ # Returns the appropriate scratchpad based on mode:
65
+ # - :enabled - returns the shared scratchpad (same for all nodes)
66
+ # - :per_node - returns node-specific scratchpad (lazy initialized)
67
+ # - :disabled - returns nil
68
+ #
69
+ # @param node_name [Symbol] Node name
70
+ # @return [Tools::Stores::ScratchpadStorage, nil] Scratchpad instance or nil if disabled
71
+ def scratchpad_for(node_name)
72
+ case @scratchpad
73
+ when :enabled
74
+ @shared_scratchpad_storage
75
+ when :per_node
76
+ # Lazy initialization per node
77
+ @node_scratchpads[node_name] ||= Tools::Stores::ScratchpadStorage.new
78
+ when :disabled
79
+ nil
80
+ end
81
+ end
82
+
83
+ # Get all scratchpad storages (for snapshot/restore)
84
+ #
85
+ # @return [Hash] { :shared => scratchpad } or { node_name => scratchpad }
86
+ def all_scratchpads
87
+ case @scratchpad
88
+ when :enabled
89
+ { shared: @shared_scratchpad_storage }
90
+ when :per_node
91
+ @node_scratchpads.dup
92
+ when :disabled
93
+ {}
94
+ end
95
+ end
96
+
97
+ # Check if scratchpad is enabled
98
+ #
99
+ # @return [Boolean]
100
+ def scratchpad_enabled?
101
+ @scratchpad != :disabled
102
+ end
103
+
104
+ # Check if scratchpad is shared between nodes (enabled mode)
105
+ #
106
+ # @return [Boolean]
107
+ def shared_scratchpad?
108
+ @scratchpad == :enabled
109
+ end
110
+
111
+ # Check if scratchpad is per-node
112
+ #
113
+ # @return [Boolean]
114
+ def per_node_scratchpad?
115
+ @scratchpad == :per_node
116
+ end
117
+
118
+ # Backward compatibility accessor
119
+ #
120
+ # @return [Tools::Stores::ScratchpadStorage, nil]
121
+ def shared_scratchpad_storage
122
+ if @scratchpad == :per_node
123
+ RubyLLM.logger.warn("NodeOrchestrator: Accessing shared_scratchpad_storage in per-node mode. Use scratchpad_for(node_name) instead.")
124
+ end
125
+ @shared_scratchpad_storage
126
+ end
127
+
38
128
  # Return the lead agent of the start node for CLI compatibility
39
129
  #
40
130
  # @return [Symbol] Lead agent of the start node
@@ -56,6 +146,9 @@ module SwarmSDK
56
146
  results = {}
57
147
  @original_prompt = prompt # Store original prompt for NodeContext
58
148
 
149
+ # Set fiber-local execution context for entire workflow
150
+ Fiber[:execution_id] = generate_execution_id
151
+
59
152
  # Setup logging if block given
60
153
  if block_given?
61
154
  # Register callback to collect logs and forward to user's block
@@ -77,6 +170,12 @@ module SwarmSDK
77
170
  node = @nodes[node_name]
78
171
  node_start_time = Time.now
79
172
 
173
+ # Set node-specific swarm_id in fiber storage
174
+ # Mini-swarms will use ||= to inherit execution_id
175
+ node_swarm_id = @swarm_id ? "#{@swarm_id}/node:#{node_name}" : nil
176
+ Fiber[:swarm_id] = node_swarm_id
177
+ Fiber[:parent_swarm_id] = @swarm_id
178
+
80
179
  # Emit node_start event
81
180
  emit_node_start(node_name, node)
82
181
 
@@ -226,13 +325,92 @@ module SwarmSDK
226
325
 
227
326
  last_result
228
327
  ensure
328
+ # NodeOrchestrator always clears (always sets up logging)
329
+ Fiber[:execution_id] = nil
330
+ Fiber[:swarm_id] = nil
331
+ Fiber[:parent_swarm_id] = nil
332
+
229
333
  # Reset logging state for next execution
230
334
  LogCollector.reset!
231
335
  LogStream.reset!
232
336
  end
233
337
 
338
+ # Create snapshot of current workflow state
339
+ #
340
+ # Returns a Snapshot object containing agent conversations, context state,
341
+ # and scratchpad data from all nodes that have been executed. The snapshot
342
+ # captures the state of agents in the agent_instance_cache (both primary and
343
+ # delegation instances), as well as scratchpad storage.
344
+ #
345
+ # Configuration (agent definitions, nodes, transformers) stays in your code
346
+ # and is NOT included in snapshots.
347
+ #
348
+ # Scratchpad behavior depends on scratchpad mode:
349
+ # - :enabled (default): single scratchpad shared across all nodes
350
+ # - :per_node: separate scratchpad per node
351
+ # - :disabled: no scratchpad data
352
+ #
353
+ # @return [Snapshot] Snapshot object with convenient serialization methods
354
+ #
355
+ # @example Save snapshot to JSON file
356
+ # orchestrator = NodeOrchestrator.new(...)
357
+ # orchestrator.execute("Build feature")
358
+ # snapshot = orchestrator.snapshot
359
+ # snapshot.write_to_file("workflow_session.json")
360
+ def snapshot
361
+ StateSnapshot.new(self).snapshot
362
+ end
363
+
364
+ # Restore workflow state from snapshot
365
+ #
366
+ # Accepts a Snapshot object, hash, or JSON string. Validates compatibility
367
+ # between snapshot and current orchestrator configuration. Restores agent
368
+ # conversations that exist in the agent_instance_cache.
369
+ #
370
+ # The orchestrator must be created with the SAME configuration (agent definitions,
371
+ # nodes) as when the snapshot was created. Only conversation state is restored.
372
+ #
373
+ # For agents with reset_context: false, restored conversations will be injected
374
+ # during node execution. Agents not in cache yet will be skipped (they haven't
375
+ # been used yet, so there's nothing to restore).
376
+ #
377
+ # @param snapshot [Snapshot, Hash, String] Snapshot object, hash, or JSON string
378
+ # @return [RestoreResult] Result with warnings about skipped agents
379
+ #
380
+ # @example Restore from Snapshot object
381
+ # orchestrator = NodeOrchestrator.new(...) # Same config as snapshot
382
+ # snapshot = Snapshot.from_file("workflow_session.json")
383
+ # result = orchestrator.restore(snapshot)
384
+ # if result.success?
385
+ # puts "All agents restored"
386
+ # else
387
+ # puts result.summary
388
+ # end
389
+ #
390
+ # Restore orchestrator state from snapshot
391
+ #
392
+ # By default, uses current system prompts from agent definitions (YAML + SDK defaults + plugin injections).
393
+ # Set preserve_system_prompts: true to use historical prompts from snapshot.
394
+ #
395
+ # @param snapshot [Snapshot, Hash, String] Snapshot object, hash, or JSON string
396
+ # @param preserve_system_prompts [Boolean] Use historical system prompts instead of current config (default: false)
397
+ # @return [RestoreResult] Result with warnings about partial restores
398
+ def restore(snapshot, preserve_system_prompts: false)
399
+ StateRestorer.new(self, snapshot, preserve_system_prompts: preserve_system_prompts).restore
400
+ end
401
+
234
402
  private
235
403
 
404
+ # Generate a unique execution ID for workflow
405
+ #
406
+ # Creates an execution ID that uniquely identifies a single orchestrator.execute() call.
407
+ # Format: "exec_workflow_{random_hex}"
408
+ #
409
+ # @return [String] Generated execution ID (e.g., "exec_workflow_a3f2b1c8")
410
+ def generate_execution_id
411
+ "exec_workflow_#{SecureRandom.hex(8)}"
412
+ end
413
+
236
414
  # Emit node_start event
237
415
  #
238
416
  # @param node_name [Symbol] Name of the node
@@ -346,26 +524,46 @@ module SwarmSDK
346
524
  # For agents with reset_context: false, injects cached instances
347
525
  # to preserve conversation history across nodes.
348
526
  #
349
- # Inherits scratchpad_enabled setting from NodeOrchestrator.
527
+ # Scratchpad behavior depends on mode:
528
+ # - :enabled - all nodes use the same scratchpad instance
529
+ # - :per_node - each node gets its own scratchpad instance
530
+ # - :disabled - no scratchpad
350
531
  #
351
532
  # @param node [Node::Builder] Node configuration
352
533
  # @return [Swarm] Configured swarm instance
353
534
  def build_swarm_for_node(node)
535
+ # Build hierarchical swarm_id if parent has one (nil auto-generates)
536
+ node_swarm_id = @swarm_id ? "#{@swarm_id}/node:#{node.name}" : nil
537
+
354
538
  swarm = Swarm.new(
355
539
  name: "#{@swarm_name}:#{node.name}",
356
- scratchpad_enabled: @scratchpad_enabled,
540
+ swarm_id: node_swarm_id,
541
+ parent_swarm_id: @swarm_id,
542
+ scratchpad: scratchpad_for(node.name),
543
+ scratchpad_mode: :enabled, # Mini-swarms always use enabled (scratchpad instance passed in)
544
+ allow_filesystem_tools: @allow_filesystem_tools,
357
545
  )
358
546
 
547
+ # Setup swarm registry if external swarms are registered
548
+ if @swarm_registry_config&.any?
549
+ registry = SwarmRegistry.new(parent_swarm_id: node_swarm_id || swarm.swarm_id)
550
+ @swarm_registry_config.each do |reg|
551
+ registry.register(reg[:name], source: reg[:source], keep_context: reg[:keep_context])
552
+ end
553
+ swarm.swarm_registry = registry
554
+ end
555
+
359
556
  # Add each agent specified in this node
360
557
  node.agent_configs.each do |config|
361
558
  agent_name = config[:agent]
362
559
  delegates_to = config[:delegates_to]
560
+ tools_override = config[:tools]
363
561
 
364
562
  # Get global agent definition
365
563
  agent_def = @agent_definitions[agent_name]
366
564
 
367
- # Clone definition with node-specific delegation
368
- node_specific_def = clone_with_delegation(agent_def, delegates_to)
565
+ # Clone definition with node-specific overrides
566
+ node_specific_def = clone_agent_for_node(agent_def, delegates_to, tools_override)
369
567
 
370
568
  swarm.add_agent(node_specific_def)
371
569
  end
@@ -379,14 +577,20 @@ module SwarmSDK
379
577
  swarm
380
578
  end
381
579
 
382
- # Clone an agent definition with different delegates_to
580
+ # Clone an agent definition with node-specific overrides
581
+ #
582
+ # Allows overriding delegation and tools per node. This enables:
583
+ # - Different delegation topology per node
584
+ # - Different tool sets per workflow stage
383
585
  #
384
586
  # @param agent_def [Agent::Definition] Original definition
385
587
  # @param delegates_to [Array<Symbol>] New delegation targets
386
- # @return [Agent::Definition] Cloned definition
387
- def clone_with_delegation(agent_def, delegates_to)
588
+ # @param tools [Array<Symbol>, nil] Tool override (nil = use global agent definition)
589
+ # @return [Agent::Definition] Cloned definition with overrides
590
+ def clone_agent_for_node(agent_def, delegates_to, tools)
388
591
  config = agent_def.to_h
389
592
  config[:delegates_to] = delegates_to
593
+ config[:tools] = tools if tools # Only override if explicitly set
390
594
  Agent::Definition.new(agent_def.name, config)
391
595
  end
392
596
 
@@ -540,18 +744,29 @@ module SwarmSDK
540
744
  # @param node [Node::Builder] Node configuration
541
745
  # @return [void]
542
746
  def cache_agent_instances(swarm, node)
543
- return unless swarm.agents # Only cache if agents were initialized
747
+ return unless swarm.agents
544
748
 
545
749
  node.agent_configs.each do |config|
546
750
  agent_name = config[:agent]
547
751
  reset_context = config[:reset_context]
548
752
 
549
- # Only cache if reset_context is false
753
+ # Only cache if reset_context: false
550
754
  next if reset_context
551
755
 
552
- # Cache the agent instance
756
+ # Cache primary agent
553
757
  agent_instance = swarm.agents[agent_name]
554
- @agent_instance_cache[agent_name] = agent_instance if agent_instance
758
+ @agent_instance_cache[:primary][agent_name] = agent_instance if agent_instance
759
+
760
+ # V7.0: Cache delegation instances atomically (together with primary)
761
+ agent_def = @agent_definitions[agent_name]
762
+ agent_def.delegates_to.each do |delegate_name|
763
+ delegation_key = "#{delegate_name}@#{agent_name}"
764
+ delegation_instance = swarm.delegation_instances[delegation_key]
765
+
766
+ if delegation_instance
767
+ @agent_instance_cache[:delegations][delegation_key] = delegation_instance
768
+ end
769
+ end
555
770
  end
556
771
  end
557
772
 
@@ -565,27 +780,79 @@ module SwarmSDK
565
780
  # @return [void]
566
781
  def inject_cached_agents(swarm, node)
567
782
  # 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
783
+ has_preserved = node.agent_configs.any? do |c|
784
+ !c[:reset_context] && (
785
+ @agent_instance_cache[:primary][c[:agent]] ||
786
+ has_cached_delegations_for?(c[:agent])
787
+ )
788
+ end
789
+ return unless has_preserved
570
790
 
571
- # Force agent initialization by accessing .agents (triggers lazy init)
572
- # Then inject cached instances
791
+ # V7.0 CRITICAL FIX: Force initialization FIRST
792
+ # Without this, @agents will be replaced by initialize_all, losing our injected instances
793
+ swarm.agent(node.agent_configs.first[:agent]) # Triggers lazy init
794
+
795
+ # Now safely inject cached instances
573
796
  agents_hash = swarm.agents
797
+ delegation_hash = swarm.delegation_instances
574
798
 
799
+ # Inject cached PRIMARY agents
575
800
  node.agent_configs.each do |config|
576
801
  agent_name = config[:agent]
577
- reset_context = config[:reset_context]
802
+ next if config[:reset_context]
578
803
 
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]
804
+ cached_agent = @agent_instance_cache[:primary][agent_name]
584
805
  next unless cached_agent
585
806
 
586
- # Inject the cached instance (replace the freshly initialized one)
807
+ # Replace freshly initialized agent with cached instance
587
808
  agents_hash[agent_name] = cached_agent
588
809
  end
810
+
811
+ # Inject cached DELEGATION instances (atomic with primary)
812
+ node.agent_configs.each do |config|
813
+ agent_name = config[:agent]
814
+ next if config[:reset_context]
815
+
816
+ agent_def = @agent_definitions[agent_name]
817
+
818
+ agent_def.delegates_to.each do |delegate_name|
819
+ delegation_key = "#{delegate_name}@#{agent_name}"
820
+ cached_delegation = @agent_instance_cache[:delegations][delegation_key]
821
+ next unless cached_delegation
822
+
823
+ # Replace freshly initialized delegation instance
824
+ # V7.0: Tool references intact - atomic caching preserves object graph
825
+ delegation_hash[delegation_key] = cached_delegation
826
+ end
827
+ end
828
+ end
829
+
830
+ def has_cached_delegations_for?(agent_name)
831
+ agent_def = @agent_definitions[agent_name]
832
+ agent_def.delegates_to.any? do |delegate_name|
833
+ delegation_key = "#{delegate_name}@#{agent_name}"
834
+ @agent_instance_cache[:delegations][delegation_key]
835
+ end
836
+ end
837
+
838
+ # Normalize scratchpad mode parameter
839
+ #
840
+ # Accepts symbols: :enabled, :per_node, or :disabled
841
+ #
842
+ # @param value [Symbol, String] Scratchpad mode (strings from YAML converted to symbols)
843
+ # @return [Symbol] Normalized mode (:enabled, :per_node, or :disabled)
844
+ # @raise [ArgumentError] If value is invalid
845
+ def normalize_scratchpad_mode(value)
846
+ # Convert strings from YAML to symbols
847
+ value = value.to_sym if value.is_a?(String)
848
+
849
+ case value
850
+ when :enabled, :per_node, :disabled
851
+ value
852
+ else
853
+ raise ArgumentError,
854
+ "Invalid scratchpad mode: #{value.inspect}. Use :enabled, :per_node, or :disabled"
855
+ end
589
856
  end
590
857
  end
591
858
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ # Helper methods for working with Procs and Lambdas
5
+ #
6
+ # Provides functionality to convert regular Proc objects into Lambdas to enable
7
+ # safe use of return statements in DSL blocks (like input/output transformers).
8
+ module ProcHelpers
9
+ class << self
10
+ # Convert a Proc to a Lambda
11
+ #
12
+ # The fundamental difference between a Proc and a Lambda is in how they handle
13
+ # return statements. In a Proc, return exits the enclosing method (or program),
14
+ # while in a Lambda, return only exits the lambda itself.
15
+ #
16
+ # This method converts a Proc to a Lambda by:
17
+ # 1. Converting the proc to an unbound method via define_method
18
+ # 2. Wrapping it in a lambda that binds and calls the method
19
+ # 3. In the method, return exits the method (not the original scope)
20
+ #
21
+ # This allows users to write natural control flow with return statements:
22
+ #
23
+ # @example
24
+ # my_proc = proc { |x| return x * 2 if x > 0; 0 }
25
+ # my_lambda = ProcHelpers.to_lambda(my_proc)
26
+ # my_lambda.call(5) # => 10 (return works safely!)
27
+ #
28
+ # @param proc [Proc] The proc to convert
29
+ # @return [Proc] A lambda with the same behavior but safe return semantics
30
+ def to_lambda(proc)
31
+ return proc if proc.lambda?
32
+
33
+ # Save local reference to proc so we can use it in module_exec/lambda scopes
34
+ source_proc = proc
35
+
36
+ # Convert proc to unbound method
37
+ # define_method with a block converts the block to a method, where return
38
+ # exits the method (not the original scope)
39
+ unbound_method = Module.new.module_exec do
40
+ instance_method(define_method(:_proc_call, &source_proc))
41
+ end
42
+
43
+ # Return lambda which binds our unbound method to correct receiver and calls it
44
+ lambda do |*args, **kwargs, &block|
45
+ # Bind method to the original proc's receiver (the context where it was defined)
46
+ # This preserves access to instance variables, local variables via closure, etc.
47
+ receiver = source_proc.binding.eval("self")
48
+ unbound_method.bind(receiver).call(*args, **kwargs, &block)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -69,139 +69,15 @@ When making changes to files, first understand the file's conventions. Mimic exi
69
69
  - When you edit something, first look at the surrounding context (especially imports/requires) to understand the choice of frameworks and libraries. Then consider how to make the given change in a way that is most consistent with existing patterns.
70
70
  - Always follow security best practices. Never introduce code that exposes or logs secrets and keys. Never commit secrets or keys to repositories.
71
71
 
72
- # Task Management
73
-
74
- You have access to the TodoWrite tool to help you manage and plan tasks. Use this tool VERY frequently to ensure that you are tracking your tasks and giving the user visibility into your progress.
75
- This tool is also EXTREMELY helpful for planning tasks, and for breaking down larger complex tasks into smaller steps. If you do not use this tool when planning, you may forget to do important tasks - and that is unacceptable.
76
-
77
- **CRITICAL WORKFLOW**: When starting a multi-step task:
78
- 1. **FIRST**: Analyze what needs to be done (search, read files, understand scope)
79
- 2. **SECOND**: Create a COMPLETE todo list with ALL known tasks before starting work
80
- 3. **THIRD**: Begin executing tasks, marking them in_progress → completed as you work
81
- 4. **ONLY add new todos** if you discover unexpected work during implementation
82
-
83
- **CRITICAL RULES FOR TODO COMPLETION**:
84
- - Mark EACH task as completed IMMEDIATELY after finishing it (do not batch updates)
85
- - You MUST complete ALL pending todos before giving your final answer to the user
86
- - If a task becomes irrelevant, remove it from the list or mark it completed with a note
87
- - NEVER leave in_progress or pending tasks when you finish responding to the user
88
- - Before giving your final response, verify all todos are marked completed
89
-
90
- Examples:
91
-
92
- <example>
93
- user: Run the build and fix any type errors
94
- assistant: I'll run the build first to identify all type errors, then create a complete todo list.
95
-
96
- [Runs build and finds 3 type errors in 3 different files]
97
-
98
- Now I'll create a complete todo list with all the work:
99
-
100
- [Uses TodoWrite to create full list:]
101
- 1. Fix type error in auth.ts:45 (in_progress)
102
- 2. Fix type error in user.ts:23 (pending)
103
- 3. Fix type error in api.ts:67 (pending)
104
- 4. Run build again to verify all fixes (pending)
105
-
106
- Starting with the first error in auth.ts...
107
- [Fixes auth.ts error]
108
-
109
- [Updates TodoWrite - marks task 1 completed, task 2 in_progress]
110
-
111
- Now fixing user.ts...
112
- [Fixes user.ts error]
113
-
114
- [Updates TodoWrite - marks task 2 completed, task 3 in_progress]
115
-
116
- Now fixing api.ts...
117
- [Fixes api.ts error]
118
-
119
- [Updates TodoWrite - marks task 3 completed, task 4 in_progress]
120
-
121
- Running build to verify...
122
- [Runs build - all errors fixed!]
123
-
124
- [Updates TodoWrite - marks task 4 completed]
125
-
126
- All type errors have been fixed and the build passes successfully.
127
- </example>
128
- In this example, the assistant: (1) analyzed scope first, (2) created COMPLETE todo list, (3) marked EACH task completed immediately after finishing it, (4) completed ALL tasks before giving final response.
129
-
130
- <example>
131
- user: Help me write a new feature that allows users to track their usage metrics and export them to various formats
132
-
133
- A: I'll help you implement a usage metrics tracking and export feature. Let me first research the codebase to understand the scope, then create a complete todo list.
134
-
135
- [Searches for existing metrics/telemetry code, examines architecture, identifies integration points]
136
-
137
- Based on my analysis, here's the complete implementation plan:
138
-
139
- [Uses TodoWrite to create full list:]
140
- 1. Create MetricsCollector class in src/metrics/ (in_progress)
141
- 2. Add database schema for metrics storage (pending)
142
- 3. Implement CSV export format (pending)
143
- 4. Implement JSON export format (pending)
144
- 5. Create MetricsService API endpoints (pending)
145
- 6. Add frontend UI for export (pending)
146
- 7. Write unit tests for collectors and exporters (pending)
147
- 8. Update documentation (pending)
148
-
149
- Now starting with the MetricsCollector class...
150
- [Implements MetricsCollector]
151
-
152
- [Updates TodoWrite - marks task 1 completed, task 2 in_progress]
153
-
154
- Adding database schema...
155
- [Creates schema]
156
-
157
- [Updates TodoWrite - marks task 2 completed, task 3 in_progress]
158
-
159
- Implementing CSV export...
160
- [Implements CSV exporter]
161
-
162
- [Updates TodoWrite - marks task 3 completed, task 4 in_progress]
163
-
164
- Implementing JSON export...
165
- [Implements JSON exporter]
166
-
167
- [Updates TodoWrite - marks task 4 completed, task 5 in_progress]
168
-
169
- Creating MetricsService API endpoints...
170
- [Implements API endpoints]
171
-
172
- [Updates TodoWrite - marks task 5 completed, task 6 in_progress]
173
-
174
- Adding frontend UI for export...
175
- [Implements UI components]
176
-
177
- [Updates TodoWrite - marks task 6 completed, task 7 in_progress]
178
-
179
- Writing unit tests...
180
- [Writes and runs tests - all pass]
181
-
182
- [Updates TodoWrite - marks task 7 completed, task 8 in_progress]
183
-
184
- Updating documentation...
185
- [Updates docs with usage examples]
186
-
187
- [Updates TodoWrite - marks task 8 completed]
188
-
189
- The metrics tracking and export feature is now complete. Users can collect metrics and export them to CSV or JSON formats through both the API and the frontend UI.
190
- </example>
191
-
192
72
  # Doing tasks
193
73
 
194
74
  The user will primarily request you perform tasks. This includes solving problems, adding new functionality, refactoring, explaining content, and more. For these tasks the following steps are recommended:
195
75
 
196
- - Use the TodoWrite tool to plan the task if required
197
76
  - Use the available search tools to understand the context and the user's query. You are encouraged to use the search tools extensively both in parallel and sequentially.
198
77
  - Implement the solution using all tools available to you
199
- - Mark each todo completed IMMEDIATELY after finishing it
200
78
  - Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the project documentation or search to determine the testing approach.
201
79
  - When you have completed a task, if there are linting or validation commands available to you, run them to ensure your work is correct. NEVER assume what these commands are - check the project documentation first.
202
80
  NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive.
203
- - Before giving your final response: Ensure ALL todos are marked completed. NEVER leave pending or in_progress tasks.
204
- - IMPORTANT: Always use the TodoWrite tool to plan and track tasks throughout the conversation.
205
81
 
206
82
  # Tool usage policy
207
83
 
@@ -211,8 +87,6 @@ NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTAN
211
87
  - If the user specifies that they want you to run tools "in parallel", you MUST send a single message with multiple tool use content blocks. For example, if you need to delegate a task to multiple agents in parallel, send a single message with multiple DelegateTask tool calls.
212
88
  - Use specialized tools instead of bash commands when possible, as this provides a better user experience. For file operations, use dedicated tools: Read for reading files instead of cat/head/tail, Edit/MultiEdit for editing instead of sed/awk, and Write for creating files instead of cat with heredoc or echo redirection. Reserve bash tools exclusively for actual system commands and terminal operations that require shell execution. NEVER use bash echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead.
213
89
 
214
- IMPORTANT: Always use the TodoWrite tool to plan and track tasks throughout the conversation.
215
-
216
90
  You MUST answer concisely with fewer than 4 lines of text (not including tool use or code generation), unless user asks for detail.
217
91
 
218
92