Class: Crimson::Agent

Inherits:
Object
  • Object
show all
Defined in:
lib/crimson/agent.rb,
lib/crimson/agent/events.rb,
lib/crimson/agent/steering.rb,
lib/crimson/agent/event_emitter.rb,
lib/crimson/agent/tool_executor.rb

Overview

Core agent loop managing conversation history, tool execution, session persistence, and event emission.

Defined Under Namespace

Modules: Events Classes: EventEmitter, SteeringManager, ToolExecutor

Constant Summary collapse

MAX_ITERATIONS =

Maximum iterations per user prompt before forcing a break.

50
HISTORY_FILE =

File name for saving/loading conversation history.

".crimson_history"
NEEDS_TOOL_PATTERNS =

Keywords that signal tool usage may be needed.

%w[
  read write edit create fix bug test run exec command search find
  file files directory folder install update delete remove patch
  config setup deploy build compile lint format check verify
  gem npm pip cargo bundle make git docker ls cat touch mkdir rm mv cp
  grep rg sed awk head tail wc diff code project src spec
  explain why how where when who which refactor implement
  list show look open
].freeze
TRIVIAL_PATTERNS =

Patterns that indicate a trivial greeting that doesn’t need tools.

%w[hi hello hey thanks thank ok yes no bye goodbye sure].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(client:, tool_registry:, system_prompt:, skill_router: nil) ⇒ Agent

Returns a new instance of Agent.

Parameters:



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/crimson/agent.rb', line 72

def initialize(client:, tool_registry:, system_prompt:, skill_router: nil)
  @client = client
  @tool_registry = tool_registry
  @system_prompt = system_prompt
  @system_prompt_builder = nil
  @skill_router = skill_router || SkillRouter.new
  @active_skills = ["coding"]
  @config = Crimson.config
  @history = []
  @events = Agent::EventEmitter.new
  @steering = Agent::SteeringManager.new
  @token_usage = { prompt: 0, completion: 0, total: 0 }
  @before_tool_call = nil
  @after_tool_call = nil
  @abort_controller = false
  @abort_signal = AbortSignal.new
  @session_manager = nil
  @session_id = nil
  @session_cwd = nil
  @last_entry_id = nil
  @session_buffer = []
  @compactor = nil
  @cost_tracker = CostTracker.new
  @cached_tool_defs = nil
  @cached_system_msg = nil
end

Instance Attribute Details

#compactorString, ... (readonly)

Returns:

  • (String, nil)

    current session ID

  • (String, nil)

    session working directory

  • (CostTracker)
  • (Compactor, nil)


62
63
64
# File 'lib/crimson/agent.rb', line 62

def compactor
  @compactor
end

#configConfig

Returns:



64
65
66
# File 'lib/crimson/agent.rb', line 64

def config
  @config
end

#cost_trackerString, ... (readonly)

Returns:

  • (String, nil)

    current session ID

  • (String, nil)

    session working directory

  • (CostTracker)
  • (Compactor, nil)


62
63
64
# File 'lib/crimson/agent.rb', line 62

def cost_tracker
  @cost_tracker
end

#define_system_prompt=(value) ⇒ Object (writeonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



66
67
68
# File 'lib/crimson/agent.rb', line 66

def define_system_prompt=(value)
  @define_system_prompt = value
end

#eventsToolRegistry, ... (readonly)

Returns:



57
58
59
# File 'lib/crimson/agent.rb', line 57

def events
  @events
end

#session_cwdString, ... (readonly)

Returns:

  • (String, nil)

    current session ID

  • (String, nil)

    session working directory

  • (CostTracker)
  • (Compactor, nil)


62
63
64
# File 'lib/crimson/agent.rb', line 62

def session_cwd
  @session_cwd
end

#session_idString, ... (readonly)

Returns:

  • (String, nil)

    current session ID

  • (String, nil)

    session working directory

  • (CostTracker)
  • (Compactor, nil)


62
63
64
# File 'lib/crimson/agent.rb', line 62

def session_id
  @session_id
end

#steeringToolRegistry, ... (readonly)

Returns:



57
58
59
# File 'lib/crimson/agent.rb', line 57

def steering
  @steering
end

#token_usageToolRegistry, ... (readonly)

Returns:



57
58
59
# File 'lib/crimson/agent.rb', line 57

def token_usage
  @token_usage
end

#tool_registryToolRegistry, ... (readonly)

Returns:



57
58
59
# File 'lib/crimson/agent.rb', line 57

def tool_registry
  @tool_registry
end

Instance Method Details

#abort!void

This method returns an undefined value.

Abort the current agent execution.



209
210
211
212
# File 'lib/crimson/agent.rb', line 209

def abort!
  @abort_signal.abort!
  @abort_controller = true
end

#after_tool_call {|tool_call, result, is_error, history| ... } ⇒ void

This method returns an undefined value.

Register a hook that runs after each tool call.

Yield Parameters:



122
123
124
# File 'lib/crimson/agent.rb', line 122

def after_tool_call(&block)
  @after_tool_call = block
end

#before_tool_call {|tool_call, args, history| ... } ⇒ void

This method returns an undefined value.

Register a hook that runs before each tool call.

Yield Parameters:



112
113
114
# File 'lib/crimson/agent.rb', line 112

def before_tool_call(&block)
  @before_tool_call = block
end

#compact!String

Force compaction of the conversation history.

Returns:

  • (String)

    status message



168
169
170
171
172
173
174
# File 'lib/crimson/agent.rb', line 168

def compact!
  return "Compaction not enabled" unless @compactor
  return "History too short to compact" if @history.length <= 5

  @history = @compactor.compact(@history, system_prompt: resolved_system_prompt)
  "Compacted history to #{@history.length} messages"
end

#continuevoid

This method returns an undefined value.

Continue the agent loop after a manual break.



189
190
191
# File 'lib/crimson/agent.rb', line 189

def continue
  run_loop
end

#enable_compaction!(client:, max_context_tokens: 100_000, model: nil, provider: nil) ⇒ void

This method returns an undefined value.

Enable context compaction with the given client for summarization.

Parameters:

  • client (Client::Base)
  • max_context_tokens (Integer) (defaults to: 100_000)
  • model (String, nil) (defaults to: nil)
  • provider (String, nil) (defaults to: nil)


157
158
159
160
161
162
163
164
# File 'lib/crimson/agent.rb', line 157

def enable_compaction!(client:, max_context_tokens: 100_000, model: nil, provider: nil)
  @compactor = Compactor.new(
    client: client,
    max_context_tokens: max_context_tokens,
    model: model || Crimson.config&.model,
    provider: provider || Crimson.config&.provider
  )
end

#follow_up(message) ⇒ void

This method returns an undefined value.

Inject a follow-up message into the current turn.

Parameters:

  • message (String)


203
204
205
# File 'lib/crimson/agent.rb', line 203

def follow_up(message)
  @steering.follow_up(Message::User.new(message))
end

#historyArray<Message::Base>

Returns a copy of the conversation history.

Returns:



241
242
243
# File 'lib/crimson/agent.rb', line 241

def history
  @history.dup
end

#history=(new_history) ⇒ Object

Parameters:



246
247
248
# File 'lib/crimson/agent.rb', line 246

def history=(new_history)
  @history = new_history.dup
end

#load_historyString

Load conversation history from a JSON file.

Returns:

  • (String)

    status message



263
264
265
266
267
268
269
270
271
272
# File 'lib/crimson/agent.rb', line 263

def load_history
  return "No saved conversation found." unless File.exist?(HISTORY_FILE)

  data = JSON.parse(File.read(HISTORY_FILE), symbolize_names: true)
  @history = data[:history].map { |msg| deserialize_message(msg) }.compact
  @token_usage = data[:token_usage] || { prompt: 0, completion: 0, total: 0 }
  "Loaded #{@history.length} messages"
rescue => e
  "Error loading history: #{e.message}"
end

#on(event_type) { ... } ⇒ void

This method returns an undefined value.

Subscribe to an agent event.

Parameters:

  • event_type (Symbol)

    event type constant

Yields:

  • handler block



103
104
105
# File 'lib/crimson/agent.rb', line 103

def on(event_type, &handler)
  @events.on(event_type, &handler)
end

#prompt(user_input) ⇒ void

This method returns an undefined value.

Process user input through the agent loop.

Parameters:

  • user_input (String)


179
180
181
182
183
184
185
# File 'lib/crimson/agent.rb', line 179

def prompt(user_input)
  @history << Message::User.new(user_input)
  append_to_session(@history.last)
  @events.emit(Agent::Events::MESSAGE_START, message: @history.last)
  @events.emit(Agent::Events::MESSAGE_END, message: @history.last)
  run_loop
end

#resetvoid

This method returns an undefined value.

Reset conversation history and token usage.



233
234
235
236
237
238
# File 'lib/crimson/agent.rb', line 233

def reset
  @history.clear
  @token_usage = { prompt: 0, completion: 0, total: 0 }
  @steering.clear_all
  @cost_tracker.reset
end

#resume_session(session_id, cwd:, session_manager: SessionManager.new) ⇒ void

This method returns an undefined value.

Resume an existing session by loading its history.

Parameters:

  • session_id (String)
  • cwd (String)
  • session_manager (SessionManager) (defaults to: SessionManager.new)


142
143
144
145
146
147
148
149
# File 'lib/crimson/agent.rb', line 142

def resume_session(session_id, cwd:, session_manager: SessionManager.new)
  @session_manager = session_manager
  entries = @session_manager.load(session_id, cwd: cwd)
  @session_id = session_id
  @session_cwd = cwd
  @history = entries.map(&:to_message).compact
  @last_entry_id = entries.last&.id
end

#save_historyString

Save conversation history to a JSON file.

Returns:

  • (String)

    status message



252
253
254
255
256
257
258
259
# File 'lib/crimson/agent.rb', line 252

def save_history
  data = {
    history: @history.map { |msg| serialize_message(msg) },
    token_usage: @token_usage
  }
  File.write(HISTORY_FILE, JSON.pretty_generate(data))
  "Conversation saved to #{HISTORY_FILE}"
end

#start_session(cwd:, session_manager: SessionManager.new) ⇒ void

This method returns an undefined value.

Start a new session for the given working directory.

Parameters:

  • cwd (String)
  • session_manager (SessionManager) (defaults to: SessionManager.new)


130
131
132
133
134
135
# File 'lib/crimson/agent.rb', line 130

def start_session(cwd:, session_manager: SessionManager.new)
  @session_manager = session_manager
  @session_id = @session_manager.create(cwd: cwd)
  @session_cwd = cwd
  @last_entry_id = nil
end

#steer(message) ⇒ void

This method returns an undefined value.

Inject a steering message into the current turn.

Parameters:

  • message (String)


196
197
198
# File 'lib/crimson/agent.rb', line 196

def steer(message)
  @steering.steer(Message::User.new(message))
end

#switch_model(model_id) ⇒ void

This method returns an undefined value.

Switch to a different model, recreating the client adapter.

Parameters:

  • model_id (String)


217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/crimson/agent.rb', line 217

def switch_model(model_id)
  @config = Config.new(
    provider: @config.provider,
    model: model_id,
    api_key: @config.api_key,
    base_url: @config.base_url,
    max_tokens: @config.max_tokens,
    thinking_level: @config.thinking_level
  )
  @client = Crimson::Client.create(@config)
  @cached_tool_defs = nil
  @cached_system_msg = nil
end