Class: LLM::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/llm/client.rb

Overview

Convenience layer over Providers::Anthropic for phantom sessions (Mneme, Melete, Mneme::L2Runner) that need a multi-round tool-use loop driven from plain Ruby objects rather than the main drain pipeline.

The main agent loop does NOT use this class — DrainJob talks to the provider directly and emits Events::LLMResponded for Events::Subscribers::LLMResponseHandler to process. The tool loop here is deliberately minimal: no events, no AASM transitions, no interrupt handling — phantom sessions don’t interact with those machineries.

Examples:

registry = Tools::Registry.new
registry.register(Tools::SaveSnapshot)
client.chat_with_tools(messages, registry: registry)

Constant Summary collapse

INTERRUPT_MESSAGE =

Synthetic tool_result text shown when a tool run is aborted by the user’s Escape press. Mirrored into the interrupt subsystem so both the bash tool and any future interrupt handler share the phrasing.

"Your human wants your attention"

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model: Anima::Settings.model, max_tokens: Anima::Settings.max_tokens, provider: nil, logger: nil) ⇒ Client

Returns a new instance of Client.

Parameters:

  • model (String) (defaults to: Anima::Settings.model)

    Anthropic model identifier (default from Settings)

  • max_tokens (Integer) (defaults to: Anima::Settings.max_tokens)

    maximum tokens in the response (default from Settings)

  • provider (Providers::Anthropic, nil) (defaults to: nil)

    injectable provider instance; defaults to a new Providers::Anthropic using credentials

  • logger (Logger, nil) (defaults to: nil)

    optional logger for tool call tracing



40
41
42
43
44
45
# File 'lib/llm/client.rb', line 40

def initialize(model: Anima::Settings.model, max_tokens: Anima::Settings.max_tokens, provider: nil, logger: nil)
  @provider = build_provider(provider)
  @model = model
  @max_tokens = max_tokens
  @logger = logger
end

Instance Attribute Details

#max_tokensInteger (readonly)

Returns maximum tokens in the response.

Returns:

  • (Integer)

    maximum tokens in the response



33
34
35
# File 'lib/llm/client.rb', line 33

def max_tokens
  @max_tokens
end

#modelString (readonly)

Returns the model identifier used for API calls.

Returns:

  • (String)

    the model identifier used for API calls



30
31
32
# File 'lib/llm/client.rb', line 30

def model
  @model
end

#providerProviders::Anthropic (readonly)

Returns the underlying API provider.

Returns:



27
28
29
# File 'lib/llm/client.rb', line 27

def provider
  @provider
end

Instance Method Details

#chat_with_tools(messages, registry:, **options) ⇒ Hash

Runs a minimal multi-round tool-use cycle: call the LLM, execute any requested tools, feed results back, repeat until the LLM produces a final text response.

Intended for phantom sessions (Mneme, Melete). No events are emitted and no persistence happens — the caller is responsible for capturing whatever state the tool runs produce.

Parameters:

  • messages (Array<Hash>)

    conversation messages in Anthropic format

  • registry (Tools::Registry)

    registered tools to make available

  • options (Hash)

    additional API parameters (e.g. system:)

Returns:

  • (Hash)

    :text (String) and :api_metrics (Hash)

Raises:



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/llm/client.rb', line 60

def chat_with_tools(messages, registry:, **options)
  messages = messages.dup
  rounds = 0
  last_api_metrics = nil

  loop do
    rounds += 1
    max_rounds = Anima::Settings.max_tool_rounds
    if rounds > max_rounds
      return {text: "[Tool loop exceeded #{max_rounds} rounds — halting]", api_metrics: last_api_metrics}
    end

    response = provider.create_message(
      model: model,
      messages: messages,
      max_tokens: max_tokens,
      tools: registry.schemas,
      include_metrics: true,
      **options
    )

    last_api_metrics = response.api_metrics if response.respond_to?(:api_metrics)

    log(:debug, "stop_reason=#{response["stop_reason"]} content_types=#{(response["content"] || []).map { |b| b["type"] }.join(",")}")

    if response["stop_reason"] == "tool_use"
      tool_results = execute_tools(response, registry)
      messages += [
        {role: "assistant", content: response["content"]},
        {role: "user", content: tool_results}
      ]
    else
      return {text: extract_text(response), api_metrics: last_api_metrics}
    end
  end
end