Module: Silas::MessageBuilder
- Defined in:
- lib/silas/message_builder.rb
Overview
Rebuilds the provider conversation deterministically from persisted rows — the rows ARE the transcript (no separate events table in v0.1). Replayed executions must produce byte-identical message arrays, so nothing here may read mutable state or the clock.
Class Method Summary collapse
-
.assistant_blocks(step, settled) ⇒ Object
Text comes from the model's own blocks; tool_use blocks are rebuilt from the settled invocations so the assistant message and the tool results that follow are always a matched set (same ids, same count).
-
.call(turn, upto_index:) ⇒ Object
Canonical provider-agnostic shape; adapters map it to their wire format.
- .step_messages(turn, upto_index:) ⇒ Object
Class Method Details
.assistant_blocks(step, settled) ⇒ Object
Text comes from the model's own blocks; tool_use blocks are rebuilt from the settled invocations so the assistant message and the tool results that follow are always a matched set (same ids, same count).
54 55 56 57 58 59 60 61 |
# File 'lib/silas/message_builder.rb', line 54 def assistant_blocks(step, settled) text = Array(step.response_blocks).select { |b| b["type"] == "text" } tools = settled.map do |inv| { "type" => "tool_call", "id" => inv.tool_call_id, "name" => inv.tool_name, "arguments" => inv.arguments || {} } end text + tools end |
.call(turn, upto_index:) ⇒ Object
Canonical provider-agnostic shape; adapters map it to their wire format.
{ role: "user", content: "..." }
{ role: "assistant", content: [ ...blocks ] }
{ role: "tool", tool_call_id:, content: ... }
NOTE: always fresh queries, never cached associations — this runs inside
a live loop where rows were created moments ago on the same objects, and
a memoized empty turn.steps silently erases the model's own history.
16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# File 'lib/silas/message_builder.rb', line 16 def call(turn, upto_index:) = [] Turn.where(session_id: turn.session_id).order(:index).each do |prior| break if prior.index >= turn.index << { role: "user", content: prior.input } .concat((prior, upto_index: nil)) end << { role: "user", content: turn.input } .concat((turn, upto_index: upto_index)) end |
.step_messages(turn, upto_index:) ⇒ Object
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/silas/message_builder.rb', line 31 def (turn, upto_index:) Step.where(turn_id: turn.id).order(:index).each_with_object([]) do |step, acc| next unless step.completed? next if upto_index && step.index >= upto_index # The settled invocations are the ledger's source of truth for what the # model actually invoked. Reconstruct the assistant's tool_use blocks # from them (not from step.response_blocks, which can drift when the # model emits parallel tool calls) so every tool_result replayed below # has a matching tool_use by construction — the provider requires it. settled = ToolInvocation.where(step_id: step.id).order(:id) .select { |inv| inv.status == "completed" || inv.status == "failed" } acc << { role: "assistant", content: assistant_blocks(step, settled) } settled.each do |inv| acc << { role: "tool", tool_call_id: inv.tool_call_id, content: inv.result } end end end |