Class: RubynCode::Agent::Conversation

Inherits:
Object
  • Object
show all
Defined in:
lib/rubyn_code/agent/conversation.rb

Overview

rubocop:disable Metrics/ClassLength – message log + incremental token/tool-ID bookkeeping

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeConversation

Returns a new instance of Conversation.



10
11
12
13
# File 'lib/rubyn_code/agent/conversation.rb', line 10

def initialize
  @messages = []
  reset_derived_state!
end

Instance Attribute Details

#messagesObject (readonly)

Returns the value of attribute messages.



8
9
10
# File 'lib/rubyn_code/agent/conversation.rb', line 8

def messages
  @messages
end

Instance Method Details

#add_assistant_message(content, tool_calls: []) ⇒ Hash

Append an assistant turn to the conversation.

Parameters:

  • content (Array<Hash>, String, nil)

    text blocks from the response

  • tool_calls (Array<Hash>) (defaults to: [])

    tool_use blocks from the response

Returns:

  • (Hash)

    the appended message



31
32
33
34
35
36
37
# File 'lib/rubyn_code/agent/conversation.rb', line 31

def add_assistant_message(content, tool_calls: [])
  blocks = normalize_content(content, tool_calls)
  message = { role: 'assistant', content: blocks }
  @messages << message
  track_added_message(message)
  message
end

#add_tool_result(tool_use_id, _tool_name, output, is_error: false) ⇒ Hash

Append a tool result turn to the conversation.

Parameters:

  • tool_use_id (String)
  • tool_name (String)
  • output (String)
  • is_error (Boolean) (defaults to: false)

Returns:

  • (Hash)

    the appended message



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/rubyn_code/agent/conversation.rb', line 46

def add_tool_result(tool_use_id, _tool_name, output, is_error: false)
  result_block = {
    type: 'tool_result',
    tool_use_id: tool_use_id,
    content: output.to_s
  }
  result_block[:is_error] = true if is_error

  # The Claude API expects tool results as a user message whose content
  # is an array of tool_result blocks.  When the previous message is
  # already a user/tool_result message we append to it so that multiple
  # tool results for the same assistant turn are batched together.
  if @messages.last && @messages.last[:role] == 'user' && tool_result_message?(@messages.last)
    @messages.last[:content] << result_block
    track_appended_block(result_block)
  else
    message = { role: 'user', content: [result_block] }
    @messages << message
    track_added_message(message)
  end

  result_block
end

#add_user_message(content) ⇒ Hash

Append a user turn to the conversation.

Parameters:

  • content (String)

Returns:

  • (Hash)

    the appended message



19
20
21
22
23
24
# File 'lib/rubyn_code/agent/conversation.rb', line 19

def add_user_message(content)
  message = { role: 'user', content: content }
  @messages << message
  track_added_message(message)
  message
end

#clear!void

This method returns an undefined value.

Reset the conversation to an empty state.



88
89
90
91
# File 'lib/rubyn_code/agent/conversation.rb', line 88

def clear!
  @messages.clear
  reset_derived_state!
end

#estimated_json_charsInteger

Character length of the JSON-serialized messages array, maintained incrementally on append so per-turn token estimation doesn’t have to re-serialize the whole history. Matches JSON.generate(messages).length.

Returns:

  • (Integer)


98
99
100
101
102
103
104
# File 'lib/rubyn_code/agent/conversation.rb', line 98

def estimated_json_chars
  @json_chars ||= @messages.sum { |msg| JSON.generate(msg).length }
  return 2 if @messages.empty?

  # "[" + per-message JSON joined by "," + "]"
  @json_chars + @messages.length + 1
end

#last_assistant_textString?

Extract the text from the most recent assistant message.

Returns:

  • (String, nil)


73
74
75
76
77
78
# File 'lib/rubyn_code/agent/conversation.rb', line 73

def last_assistant_text
  assistant_msg = @messages.reverse_each.find { |m| m[:role] == 'assistant' }
  return nil unless assistant_msg

  extract_text(assistant_msg[:content])
end

#lengthInteger

Returns:

  • (Integer)


81
82
83
# File 'lib/rubyn_code/agent/conversation.rb', line 81

def length
  @messages.length
end

#refresh_derived_state!void

This method returns an undefined value.

Drops cached serialization/tool-ID bookkeeping. Must be called after messages are mutated in place from outside this class (e.g. by Context::MicroCompact); the caches rebuild lazily on next access.



111
112
113
# File 'lib/rubyn_code/agent/conversation.rb', line 111

def refresh_derived_state!
  reset_derived_state!
end

#replace!(new_messages) ⇒ Object

Replace messages with a new array (used after compaction).



153
154
155
156
# File 'lib/rubyn_code/agent/conversation.rb', line 153

def replace!(new_messages)
  @messages.replace(new_messages)
  reset_derived_state!
end

#to_api_formatArray<Hash>

Return the messages array formatted for the Claude API. Ensures proper role alternation and content structure.

Returns:

  • (Array<Hash>)


119
120
121
122
123
124
125
126
127
128
# File 'lib/rubyn_code/agent/conversation.rb', line 119

def to_api_format
  formatted = @messages.map do |msg|
    {
      role: msg[:role],
      content: format_content(msg[:content])
    }
  end

  repair_orphaned_tool_uses(formatted)
end

#undo_last!void

This method returns an undefined value.

Remove the last user + assistant exchange. Useful for undo. If the last two messages are assistant then user (most recent first), removes both. Otherwise removes only the last message.



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/rubyn_code/agent/conversation.rb', line 135

def undo_last!
  return if @messages.empty?

  # Walk backwards and remove the most recent user+assistant pair.
  # The typical pattern is: [..., user, assistant] or
  # [..., assistant, user(tool_results)].
  removed = 0
  while @messages.any? && removed < 2
    last = @messages.last
    break if removed == 1 && last[:role] != 'assistant' && last[:role] != 'user'

    @messages.pop
    removed += 1
  end
  reset_derived_state!
end