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



34
35
36
37
38
39
40
# File 'lib/rubyn_code/agent/conversation.rb', line 34

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



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

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, Array<Hash>)

    either a plain string or an array of content blocks (text, image, etc.). Strings get wrapped as [{ type: 'text', text: content }] only if there's a planned Array; we keep raw String for backward compatibility with the API.

Returns:

  • (Hash)

    the appended message



22
23
24
25
26
27
# File 'lib/rubyn_code/agent/conversation.rb', line 22

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.



91
92
93
94
# File 'lib/rubyn_code/agent/conversation.rb', line 91

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)


101
102
103
104
105
106
107
# File 'lib/rubyn_code/agent/conversation.rb', line 101

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)


76
77
78
79
80
81
# File 'lib/rubyn_code/agent/conversation.rb', line 76

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)


84
85
86
# File 'lib/rubyn_code/agent/conversation.rb', line 84

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.



114
115
116
# File 'lib/rubyn_code/agent/conversation.rb', line 114

def refresh_derived_state!
  reset_derived_state!
end

#replace!(new_messages) ⇒ Object

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



156
157
158
159
# File 'lib/rubyn_code/agent/conversation.rb', line 156

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

#to_aObject Also known as: to_ary

Alias for messages. Common shorthand for callers that want to treat the conversation as an Array of message hashes.



163
164
165
# File 'lib/rubyn_code/agent/conversation.rb', line 163

def to_a
  @messages
end

#to_api_formatArray<Hash>

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

Returns:

  • (Array<Hash>)


122
123
124
125
126
127
128
129
130
131
# File 'lib/rubyn_code/agent/conversation.rb', line 122

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.



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

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