Class: Silas::Adapters::RubyLLM

Inherits:
Base
  • Object
show all
Defined in:
lib/silas/adapters/ruby_llm.rb

Overview

The :ruby_llm adapter: ONE model call per step, streamed, with the tool calls handed back unexecuted.

RubyLLM's Chat#complete runs the whole agentic loop — model, run tools, feed results back, model again. Silas needs a single move, because the step boundary IS the durability boundary (checkpoint, ledger, park). So Chat is used as the BUILDER it is — it owns model resolution, schema normalisation, system instructions and message construction — and the execution goes one layer down to Provider#complete, which is exactly what Chat itself calls for a single turn.

Everything here is RubyLLM's public API: Chat's attr_readers (model, messages, tools, schema, tool_prefs), Provider.resolve, and Provider#complete. (Until 0.5 this used tool proxies that threw RubyLLM::Tool::Halt to abort the loop from inside — RubyLLM 2.0 removes Halt precisely because the loop became caller-controlled, so this binding is both simpler now and the forward-compatible one.)

Defined Under Namespace

Classes: SchemaProxy

Instance Method Summary collapse

Instance Method Details

#execute_step(context, &on_event) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/silas/adapters/ruby_llm.rb', line 21

def execute_step(context, &on_event)
  chat = build_chat(context)

  response = Silas.instrument(:step,
                              turn_id: context[:turn]&.id,
                              index: context[:index],
                              model: context[:model]) do
    if on_event
      # Fires before the HTTP request, matching what RubyLLM's
      # before_message callback did under streaming — but ours, and
      # explicitly ordered rather than incidentally so.
      on_event.call(Event.new(type: :message_start, payload: {}))
      complete(chat) do |chunk|
        # Chunks carrying tool-call fragments have nil/empty content —
        # only text streams.
        text = chunk.content
        on_event.call(Event.new(type: :text_delta, payload: { text: text })) if text.is_a?(String) && !text.empty?
      end
    else
      complete(chat)
    end
  end

  to_result(response, schema: chat.schema)
end