Class: PendingMessage

Inherits:
ApplicationRecord show all
Defined in:
app/models/pending_message.rb

Overview

A message waiting to enter a session’s conversation history. Pending messages live in their own table — they are NOT part of the message stream and have no database ID that could interleave with tool_call/tool_response pairs.

Created when a message arrives while the session is processing. Promoted to a real Message (delete + create in transaction) when the current agent loop completes, giving the new message an ID that naturally follows the tool batch.

Each pending message knows its source (source_type, source_name) and how to serialize itself for the LLM conversation via #to_llm_messages. Non-user messages (sub-agent results, recalled skills, workflows, recall, goal events) become synthetic tool_use/tool_result pairs so the LLM sees “a tool I invoked returned a result” rather than “a user wrote me.”

Constant Summary collapse

SUBAGENT_TOOL =

Synthetic tool names used in tool_use/tool_result pairs injected into the parent LLM conversation when non-user messages are promoted. These tools don’t exist in the agent’s registry — the agent sees them as its own past actions (phantom tool calls).

"subagent_message"
RECALL_SKILL_TOOL =
"recall_skill"
RECALL_WORKFLOW_TOOL =
"recall_workflow"
RECALL_MEMORY_TOOL =
"recall_memory"
RECALL_GOAL_TOOL =
"recall_goal"
PHANTOM_PAIR_TYPES =

Source types that produce phantom tool_use/tool_result pairs on promotion. User messages produce plain text blocks instead.

%w[subagent skill workflow recall goal].freeze
PHANTOM_TOOL_NAMES =

Maps each phantom pair source type to its synthetic tool name.

{
  "subagent" => SUBAGENT_TOOL,
  "skill" => RECALL_SKILL_TOOL,
  "workflow" => RECALL_WORKFLOW_TOOL,
  "recall" => RECALL_MEMORY_TOOL,
  "goal" => RECALL_GOAL_TOOL
}.freeze
PHANTOM_TOOL_INPUTS =

Maps each phantom pair source type to a lambda building its tool input.

{
  "subagent" => ->(name) { {from: name} },
  "skill" => ->(name) { {skill: name} },
  "workflow" => ->(name) { {workflow: name} },
  "recall" => ->(name) { {message_id: name.to_i} },
  "goal" => ->(name) { {goal_id: name.to_i} }
}.freeze

Instance Method Summary collapse

Instance Method Details

#display_contentString

Content formatted for display and history persistence. Sub-agent messages include an attribution prefix. Skill/workflow messages include a recall label. User messages pass through unchanged.

Returns:

  • (String)


118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'app/models/pending_message.rb', line 118

def display_content
  case source_type
  when "subagent"
    format(Tools::ResponseTruncator::ATTRIBUTION_FORMAT, source_name, content)
  when "skill"
    "[recalled skill: #{source_name}]\n#{content}"
  when "workflow"
    "[recalled workflow: #{source_name}]\n#{content}"
  when "goal"
    "[goal #{source_name}]\n#{content}"
  else
    content
  end
end

#goal?Boolean

Returns true when this message carries a goal event.

Returns:

  • (Boolean)

    true when this message carries a goal event



89
90
91
# File 'app/models/pending_message.rb', line 89

def goal?
  source_type == "goal"
end

#phantom_pair?Boolean

Returns true when promotion produces phantom tool_use/tool_result pairs.

Returns:

  • (Boolean)

    true when promotion produces phantom tool_use/tool_result pairs



94
95
96
# File 'app/models/pending_message.rb', line 94

def phantom_pair?
  source_type.in?(PHANTOM_PAIR_TYPES)
end

#phantom_tool_inputHash

Phantom tool input hash for DB persistence and LLM injection.

Returns:

  • (Hash)

    tool input hash



109
110
111
# File 'app/models/pending_message.rb', line 109

def phantom_tool_input
  PHANTOM_TOOL_INPUTS.fetch(source_type).call(source_name)
end

#phantom_tool_nameString

Phantom tool name for DB persistence and LLM injection. Each phantom pair source type maps to a synthetic tool name.

Returns:

  • (String)

    phantom tool name



102
103
104
# File 'app/models/pending_message.rb', line 102

def phantom_tool_name
  PHANTOM_TOOL_NAMES.fetch(source_type)
end

#recall?Boolean

Returns true when this message is an associative recall phantom pair.

Returns:

  • (Boolean)

    true when this message is an associative recall phantom pair



84
85
86
# File 'app/models/pending_message.rb', line 84

def recall?
  source_type == "recall"
end

#skill?Boolean

Returns true when this message carries recalled skill content.

Returns:

  • (Boolean)

    true when this message carries recalled skill content



74
75
76
# File 'app/models/pending_message.rb', line 74

def skill?
  source_type == "skill"
end

#subagent?Boolean

Returns true when this message originated from a sub-agent.

Returns:

  • (Boolean)

    true when this message originated from a sub-agent



69
70
71
# File 'app/models/pending_message.rb', line 69

def subagent?
  source_type == "subagent"
end

#to_llm_messagesArray<Hash>, String

Builds LLM message hashes for this pending message.

Phantom pair types become synthetic tool_use/tool_result pairs so the LLM sees them as its own past invocations. User messages return plain content for injection as text blocks within the current tool_results turn.

Returns:

  • (Array<Hash>)

    synthetic tool pair for phantom pair types

  • (String)

    raw content for user messages



141
142
143
144
145
# File 'app/models/pending_message.rb', line 141

def to_llm_messages
  return content unless phantom_pair?

  build_phantom_pair(phantom_tool_name, phantom_tool_input)
end

#user?Boolean

Returns true when this is a plain user message.

Returns:

  • (Boolean)

    true when this is a plain user message



64
65
66
# File 'app/models/pending_message.rb', line 64

def user?
  source_type == "user"
end

#workflow?Boolean

Returns true when this message carries recalled workflow content.

Returns:

  • (Boolean)

    true when this message carries recalled workflow content



79
80
81
# File 'app/models/pending_message.rb', line 79

def workflow?
  source_type == "workflow"
end