Class: SwarmSDK::V3::Agent
- Inherits:
-
Object
- Object
- SwarmSDK::V3::Agent
- Defined in:
- lib/swarm_sdk/v3/agent.rb
Overview
Runtime agent with built-in memory
The Agent ties together RubyLLM::Chat, the memory system, and tools. The LLM’s context window is a staging area — older turns get consolidated into memory cards, and retrieval brings relevant memory back on demand.
## Interruption Safety
The agent supports safe interruption via #interrupt!, which raises ‘Async::Stop` in the fiber running #ask. When adding new features to the agent, follow these rules:
-
**Interruptible phases** (LLM calls, tool execution, streaming): Code here can be interrupted at any fiber yield point. Do NOT leave shared state half-updated — use snapshot/restore or flags to detect incomplete operations in ‘ensure` blocks.
-
**Uninterruptible phases** (memory writes, STM capture, eviction): Wrap in ‘Async::Task.current.defer_stop { }` to defer `Async::Stop` until the block completes. Use this for any multi-step I/O that must be atomic (e.g., writing cards + saving the index).
-
**New instance state**: If a new feature adds state that is modified during #ask, ensure it is either rolled back on interruption (via ‘ensure`) or protected with `defer_stop`.
-
Subprocesses: Any code that spawns a subprocess (Open3, etc.) must terminate it in an ‘ensure` block. `Async::Stop` bypasses `rescue StandardError` — only `ensure` is guaranteed to run.
Direct Known Subclasses
Instance Attribute Summary collapse
-
#definition ⇒ AgentDefinition
readonly
Immutable agent configuration.
-
#id ⇒ String
readonly
Unique instance identifier (name_<hex>).
-
#loaded_skills ⇒ Array<Skills::Manifest>
readonly
Loaded skills (available after first ask).
Instance Method Summary collapse
-
#ask(prompt, output_schema: nil) {|event| ... } ⇒ RubyLLM::Message?
Send a message to the agent and get a response.
-
#clear(clear_memory: false) ⇒ void
Reset conversation and optionally clear memory.
-
#clear_steering_queue ⇒ void
Clear all queued steering messages.
-
#defrag! {|event| ... } ⇒ Hash?
Run memory defragmentation (compression, consolidation, promotion, pruning).
-
#initialize(definition) ⇒ Agent
constructor
Create a new agent.
-
#initialized? ⇒ Boolean
Whether the agent has been initialized.
-
#interrupt! ⇒ Boolean?
Stop whatever the agent is doing.
-
#loop(kickoff:, iterate:, max_iterations: 10, convergence_threshold: 0.95, converge: true) {|event| ... } ⇒ Loop::Result
Run an iterative refinement loop over the agent.
-
#memory ⇒ Memory::Store?
Read-only access to the memory store.
-
#memory_read_only? ⇒ Boolean
Whether memory operations are read-only.
-
#messages ⇒ Array<Hash>
Get recent messages (STM buffer).
-
#name ⇒ Symbol
Agent name from definition.
-
#running? ⇒ Boolean
Whether the agent is currently executing an ask() call.
-
#steer(message) ⇒ void
Queue a high-priority message that interrupts the current tool batch.
-
#tokens ⇒ Hash
Token usage statistics.
Constructor Details
#initialize(definition) ⇒ Agent
Create a new agent
Lazy-initializes the RubyLLM::Chat and memory system on first ask().
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/swarm_sdk/v3/agent.rb', line 81 def initialize(definition) @definition = definition @id = "#{definition.name}_#{SecureRandom.hex(3)}" @chat = nil @memory_store = nil @stm_buffer = [] @turn_counter = 0 @total_input_tokens = 0 @total_output_tokens = 0 @initialized = false @semaphore = Async::Semaphore.new(1) @current_task = nil @pending_ingestion = nil @steering_queue = [] @loaded_skills = nil @base_system_prompt = nil @mcp_connectors = [] @hooks = Hooks::Runner.new(definition.hooks) end |
Instance Attribute Details
#definition ⇒ AgentDefinition (readonly)
Returns Immutable agent configuration.
68 69 70 |
# File 'lib/swarm_sdk/v3/agent.rb', line 68 def definition @definition end |
#id ⇒ String (readonly)
Returns Unique instance identifier (name_<hex>).
71 72 73 |
# File 'lib/swarm_sdk/v3/agent.rb', line 71 def id @id end |
#loaded_skills ⇒ Array<Skills::Manifest> (readonly)
Returns Loaded skills (available after first ask).
74 75 76 |
# File 'lib/swarm_sdk/v3/agent.rb', line 74 def loaded_skills @loaded_skills end |
Instance Method Details
#ask(prompt, output_schema: nil) {|event| ... } ⇒ RubyLLM::Message?
Send a message to the agent and get a response
The ask() flow:
-
Lazy-initialize (create chat, memory, tools)
-
Retrieve relevant memory cards for the prompt
-
Build working context (system prompt + memory + recent turns)
-
Execute via RubyLLM::Chat (handles tool loop internally)
-
Capture turn in STM buffer
-
Ingest turn into memory (async)
-
Evict old turns from STM if buffer exceeds limit
-
Emit events
Supports safe interruption via #interrupt!. When interrupted, returns nil and leaves the agent in a consistent state for the next ask() call. Check #running? to see if an ask is in progress.
141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 |
# File 'lib/swarm_sdk/v3/agent.rb', line 141 def ask(prompt, output_schema: nil, &block) with_block_emitter(block) do Sync do |task| @semaphore.acquire do @current_task = task begin lazy_initialize! before_result = @hooks.run(:before_ask, Hooks::Context.new( event: :before_ask, agent_name: @definition.name, prompt: prompt, )) if before_result.halt? nil else prompt = before_result.value if before_result.replace? response = execute_turn(prompt, output_schema: output_schema) @hooks.run(:after_ask, Hooks::Context.new( event: :after_ask, agent_name: @definition.name, prompt: prompt, response: response, )) @hooks.run(:on_stop, Hooks::Context.new( event: :on_stop, agent_name: @definition.name, response: response, )) response end rescue Async::Stop EventStream.emit(type: "agent_interrupted", agent: @id, turn: @turn_counter) nil ensure @current_task = nil end end end end end |
#clear(clear_memory: false) ⇒ void
This method returns an undefined value.
Reset conversation and optionally clear memory
256 257 258 259 260 261 262 263 264 265 266 267 268 |
# File 'lib/swarm_sdk/v3/agent.rb', line 256 def clear(clear_memory: false) wait_for_pending_ingestion disconnect_mcp_servers @stm_buffer.clear @steering_queue.clear @turn_counter = 0 @chat&.(preserve_system_prompt: true) return unless clear_memory && @memory_store # Clear memory by reinitializing the adapter @memory_store = nil end |
#clear_steering_queue ⇒ void
This method returns an undefined value.
Clear all queued steering messages
241 242 243 |
# File 'lib/swarm_sdk/v3/agent.rb', line 241 def clear_steering_queue @steering_queue.clear end |
#defrag! {|event| ... } ⇒ Hash?
Run memory defragmentation (compression, consolidation, promotion, pruning)
Call this between sessions, on a schedule, or whenever appropriate. Does not run during conversation turns — the SDK user controls when.
400 401 402 403 404 405 406 407 408 409 410 411 412 |
# File 'lib/swarm_sdk/v3/agent.rb', line 400 def defrag!(&block) return unless @definition.memory_enabled? with_block_emitter(block) do Sync do @semaphore.acquire do lazy_initialize! wait_for_pending_ingestion @memory_store.defrag! end end end end |
#initialized? ⇒ Boolean
Whether the agent has been initialized
287 288 289 |
# File 'lib/swarm_sdk/v3/agent.rb', line 287 def initialized? @initialized end |
#interrupt! ⇒ Boolean?
Stop whatever the agent is doing
Raises Async::Stop in the fiber running ask(). Safe to call from another Async fiber in the same reactor. Idempotent — returns nil if the agent is idle.
198 199 200 201 202 203 |
# File 'lib/swarm_sdk/v3/agent.rb', line 198 def interrupt! return unless @current_task @current_task.stop true end |
#loop(kickoff:, iterate:, max_iterations: 10, convergence_threshold: 0.95, converge: true) {|event| ... } ⇒ Loop::Result
Run an iterative refinement loop over the agent
Executes a kickoff prompt followed by repeated iterate prompts, optionally checking for convergence via embedding similarity between consecutive responses. Each iteration is a normal ask() call — hooks fire, memory ingests, and events stream normally.
352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 |
# File 'lib/swarm_sdk/v3/agent.rb', line 352 def loop(kickoff:, iterate:, max_iterations: 10, convergence_threshold: 0.95, converge: true, &block) validate_loop_params!(max_iterations, convergence_threshold) = converge ? : nil ask_callable = ->(prompt) { ask(prompt, &block) } executor = Loop::Executor.new( ask_callable: ask_callable, embedder: , agent_id: @id, ) # Wrap with block emitter so loop lifecycle events # (loop_started, loop_iteration_completed, loop_completed) # reach the caller's block. Each ask() inside the executor # will also set/restore the block emitter for its own events. with_block_emitter(block) do executor.run( kickoff: kickoff, iterate: iterate, max_iterations: max_iterations, convergence_threshold: convergence_threshold, converge: converge, ) end end |
#memory ⇒ Memory::Store?
Read-only access to the memory store
294 295 296 |
# File 'lib/swarm_sdk/v3/agent.rb', line 294 def memory @memory_store end |
#memory_read_only? ⇒ Boolean
Whether memory operations are read-only
Subtask agents override this to return true, preventing access counter updates during context building.
304 305 306 |
# File 'lib/swarm_sdk/v3/agent.rb', line 304 def memory_read_only? false end |
#messages ⇒ Array<Hash>
Get recent messages (STM buffer)
248 249 250 |
# File 'lib/swarm_sdk/v3/agent.rb', line 248 def @stm_buffer.dup end |
#name ⇒ Symbol
Agent name from definition
280 281 282 |
# File 'lib/swarm_sdk/v3/agent.rb', line 280 def name @definition.name end |
#running? ⇒ Boolean
Whether the agent is currently executing an ask() call
Returns true only while the agent holds the semaphore and is actively processing a turn. Useful for deciding whether to call #interrupt!.
215 216 217 |
# File 'lib/swarm_sdk/v3/agent.rb', line 215 def running? !@current_task.nil? end |
#steer(message) ⇒ void
This method returns an undefined value.
Queue a high-priority message that interrupts the current tool batch
Steering messages are injected after the current tool completes, skipping any remaining tools in the batch. The message is delivered as a ‘role: user` message before the next LLM call.
Use this for urgent interruptions that should preempt normal execution.
234 235 236 |
# File 'lib/swarm_sdk/v3/agent.rb', line 234 def steer() @steering_queue << end |
#tokens ⇒ Hash
Token usage statistics
273 274 275 |
# File 'lib/swarm_sdk/v3/agent.rb', line 273 def tokens { input: @total_input_tokens, output: @total_output_tokens } end |