Class: PendingMessage
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- PendingMessage
- 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
-
#display_content ⇒ String
Content formatted for display and history persistence.
-
#goal? ⇒ Boolean
True when this message carries a goal event.
-
#phantom_pair? ⇒ Boolean
True when promotion produces phantom tool_use/tool_result pairs.
-
#phantom_tool_input ⇒ Hash
Phantom tool input hash for DB persistence and LLM injection.
-
#phantom_tool_name ⇒ String
Phantom tool name for DB persistence and LLM injection.
-
#recall? ⇒ Boolean
True when this message is an associative recall phantom pair.
-
#skill? ⇒ Boolean
True when this message carries recalled skill content.
-
#subagent? ⇒ Boolean
True when this message originated from a sub-agent.
-
#to_llm_messages ⇒ Array<Hash>, String
Builds LLM message hashes for this pending message.
-
#user? ⇒ Boolean
True when this is a plain user message.
-
#workflow? ⇒ Boolean
True when this message carries recalled workflow content.
Instance Method Details
#display_content ⇒ String
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.
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.
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.
94 95 96 |
# File 'app/models/pending_message.rb', line 94 def phantom_pair? source_type.in?(PHANTOM_PAIR_TYPES) end |
#phantom_tool_input ⇒ Hash
Phantom tool input hash for DB persistence and LLM injection.
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_name ⇒ String
Phantom tool name for DB persistence and LLM injection. Each phantom pair source type maps to a synthetic 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.
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.
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.
69 70 71 |
# File 'app/models/pending_message.rb', line 69 def subagent? source_type == "subagent" end |
#to_llm_messages ⇒ Array<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.
141 142 143 144 145 |
# File 'app/models/pending_message.rb', line 141 def 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.
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.
79 80 81 |
# File 'app/models/pending_message.rb', line 79 def workflow? source_type == "workflow" end |