Class: LLM::Context

Inherits:
Object
  • Object
show all
Includes:
Deserializer, Serializer
Defined in:
lib/llm/context.rb,
lib/llm/context/serializer.rb,
lib/llm/context/deserializer.rb

Overview

LLM::Context is the stateful execution boundary in llm.rb.

It holds the evolving runtime state for an LLM workflow: conversation history, tool calls and returns, schema and streaming configuration, accumulated usage, and request ownership for interruption.

This is broader than prompt context alone. A context is the object that lets one-off prompts, streaming turns, tool execution, persistence, retries, and serialized long-lived workflows all run through the same model.

A context can drive the chat completions API that all providers support or the Responses API on providers that expose it.

Examples:

#!/usr/bin/env ruby
require "llm"

llm = LLM.openai(key: ENV["KEY"])
ctx = LLM::Context.new(llm)

prompt = LLM::Prompt.new(llm) do
  system "Be concise and show your reasoning briefly."
  user "If a train goes 60 mph for 1.5 hours, how far does it travel?"
  user "Now double the speed for the same time."
end

ctx.talk(prompt)
ctx.messages.each { |m| puts "[#{m.role}] #{m.content}" }

Defined Under Namespace

Modules: Deserializer, Serializer

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Deserializer

#deserialize, #deserialize_message

Constructor Details

#initialize(llm, params = {}) ⇒ Context

Returns a new instance of Context.

Parameters:

  • llm (LLM::Provider)

    A provider

  • params (Hash) (defaults to: {})

    The parameters to maintain throughout the conversation. Any parameter the provider supports can be included and not only those listed here.

Options Hash (params):

  • :mode (Symbol)

    Defaults to :responses for OpenAI, otherwise it defaults to :completions.

  • :model (String)

    Defaults to the provider's default model

  • :tools (Array<LLM::Function>, nil)

    Defaults to nil

  • :skills (Array<String>, nil)

    Defaults to nil



84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/llm/context.rb', line 84

def initialize(llm, params = {})
  @llm = llm
  @mode = params.delete(:mode) || (llm.name == :openai ? :responses : :completions)
  @compactor = params.delete(:compactor)
  @guard = params.delete(:guard)
  @transformer = params.delete(:transformer)
  tools = [*params.delete(:tools), *load_skills(params.delete(:skills))]
  @params = {model: llm.default_model, schema: nil}.compact.merge!(params)
  @params[:tools] = tools unless tools.empty?
  @params[:store] ||= false if @mode == :responses
  @messages = LLM::Buffer.new(llm)
  extra = @params.slice(:model, :tools).merge!(ctx: self, tracer:)
  @params[:stream] = LLM::Stream.try(@params[:stream], extra:)
end

Instance Attribute Details

#compactedBoolean Also known as: compacted?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns whether the context has been compacted and no later model response has cleared that state.

Returns:

  • (Boolean)


129
130
131
# File 'lib/llm/context.rb', line 129

def compacted
  @compacted
end

#llmLLM::Provider (readonly)

Returns a provider

Returns:



64
65
66
# File 'lib/llm/context.rb', line 64

def llm
  @llm
end

#messagesLLM::Buffer<LLM::Message> (readonly)

Returns the accumulated message history for this context



59
60
61
# File 'lib/llm/context.rb', line 59

def messages
  @messages
end

#modeSymbol (readonly)

Returns the context mode

Returns:

  • (Symbol)


69
70
71
# File 'lib/llm/context.rb', line 69

def mode
  @mode
end

Instance Method Details

#ask(prompt, options = {}) {|String| ... } ⇒ LLM::Response

Ask a question and return the content string directly. Accepts with: for file attachments and a block for streaming. This interface is compatible with RubyLLM's ask method.

Parameters:

  • prompt (String)
  • options (Hash) (defaults to: {})

Options Hash (options):

  • :with (String, Array<String>, nil)

    File path(s) to attach

  • :stream (#<<, LLM::Stream, nil)

    A stream target

Yields:

  • (String)

    content chunks when streaming

Returns:



223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/llm/context.rb', line 223

def ask(prompt, options = {}, &block)
  options = {with: nil, stream: nil}.merge!(options || {})
  with, stream = options.values_at(:with, :stream)
  prompt = with ? [prompt, [*with].map { local_file(_1) }] : prompt
  target = if block
    blk = block.dup
    blk.singleton_class.alias_method(:<<, :call)
    blk
  else
    stream
  end
  target ? talk(prompt, stream: target) : talk(prompt)
end

#compactorLLM::Compactor

Returns a context compactor This feature is inspired by the compaction approach developed by General Intelligence Systems.

Returns:



111
112
113
114
# File 'lib/llm/context.rb', line 111

def compactor
  @compactor = LLM::Compactor.new(self, @compactor || {}) unless LLM::Compactor === @compactor
  @compactor
end

#compactor=(compactor) ⇒ LLM::Compactor, ...

Sets a context compactor or compactor config

Parameters:

Returns:



120
121
122
# File 'lib/llm/context.rb', line 120

def compactor=(compactor)
  @compactor = compactor
end

#context_windowInteger

Note:

This method returns 0 when the provider or model can't be found within Registry.

Returns the model's context window. The context window is the maximum amount of input and output tokens a model can consider in a single request.

Returns:

  • (Integer)


496
497
498
499
500
501
502
503
# File 'lib/llm/context.rb', line 496

def context_window
  LLM
    .registry_for(llm)
    .limit(model:)
    .context
rescue LLM::NoSuchModelError, LLM::NoSuchRegistryError
  0
end

#costLLM::Cost

Returns an approximate cost for a given context based on both the provider, and model

Returns:

  • (LLM::Cost)

    Returns an approximate cost for a given context based on both the provider, and model



484
485
486
# File 'lib/llm/context.rb', line 484

def cost
  LLM::Cost.from(self)
end

#functionsArray<LLM::Function>

Returns an array of functions that can be called

Returns:



248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/llm/context.rb', line 248

def functions
  return_ids = returns.map(&:id)
  @messages
    .select(&:assistant?)
    .flat_map do |msg|
      fns = msg.functions.select { _1.pending? && !return_ids.include?(_1.id) }
      fns.each do |fn|
        fn.tracer = tracer
        fn.model  = msg.model
      end
    end.extend(LLM::Function::Array)
end

#functions?Boolean

Returns whether there is pending tool work in this context. This prefers queued streamed tool work when present, and otherwise falls back to unresolved functions derived from the message history.

Returns:

  • (Boolean)


266
267
268
269
# File 'lib/llm/context.rb', line 266

def functions?
  pending = queue
  (pending && !pending.empty?) || functions.any?
end

#guard#call?

Returns a guard, if configured.

Guards are context-level supervisors for agentic execution. A guard can inspect the runtime state and decide whether pending tool work should be blocked before the context keeps looping.

The built-in implementation is LLM::LoopGuard, which detects repeated tool-call patterns and turns them into in-band LLM::GuardError tool returns.

Returns:

  • (#call, nil)


144
145
146
147
148
149
# File 'lib/llm/context.rb', line 144

def guard
  return if @guard.nil? || @guard == false
  @guard = LLM::LoopGuard.new if @guard == true
  @guard = LLM::LoopGuard.new(@guard) if Hash === @guard
  @guard
end

#guard=(guard) ⇒ #call, ...

Sets a guard or guard config.

Guards must implement call(ctx) and return either nil or a warning string. Returning a warning tells the context to block pending tool work with guarded tool errors instead of continuing the loop.

Parameters:

  • guard (#call, Hash, Boolean, nil)

Returns:

  • (#call, Hash, Boolean, nil)


160
161
162
# File 'lib/llm/context.rb', line 160

def guard=(guard)
  @guard = guard
end

#image_url(url) ⇒ LLM::Object

Recongize an object as a URL to an image

Parameters:

  • url (String)

    The URL

Returns:



395
396
397
# File 'lib/llm/context.rb', line 395

def image_url(url)
  LLM::Object.from(value: url, kind: :image_url)
end

#inspectString

Returns:

  • (String)


239
240
241
242
243
# File 'lib/llm/context.rb', line 239

def inspect
  "#<#{LLM::Utils.object_id(self)} " \
  "@llm=#{@llm.class}, @mode=#{@mode.inspect}, @params=#{@params.inspect}, " \
  "@messages=#{@messages.inspect}>"
end

#interrupt!nil Also known as: cancel!

Interrupt the active request, if any. This is inspired by Go's context cancellation model.

Returns:

  • (nil)


336
337
338
339
340
341
342
343
344
345
# File 'lib/llm/context.rb', line 336

def interrupt!
  pending = functions.to_a
  llm.interrupt!(@owner)
  queue&.interrupt!
  return if pending.empty?
  pending.each(&:interrupt!)
  returns = pending.map { _1.cancel(reason: "function call cancelled") }
  @messages << LLM::Message.new(@llm.tool_role, returns)
  nil
end

#local_file(path) ⇒ LLM::Object

Recongize an object as a local file

Parameters:

  • path (String)

    The path

Returns:



405
406
407
# File 'lib/llm/context.rb', line 405

def local_file(path)
  LLM::Object.from(value: LLM.File(path), kind: :local_file)
end

#modelString

Returns the model a Context is actively using

Returns:

  • (String)


444
445
446
# File 'lib/llm/context.rb', line 444

def model
  messages.find(&:assistant?)&.model || @params[:model]
end

#paramsHash

Returns the default params for this context

Returns:

  • (Hash)


102
103
104
# File 'lib/llm/context.rb', line 102

def params
  @params.dup
end

#prompt(&b) ⇒ LLM::Prompt Also known as: build_prompt

Build a role-aware prompt for a single request.

Prefer this method over #build_prompt. The older method name is kept for backward compatibility.

Examples:

prompt = ctx.prompt do
  system "Your task is to assist the user"
  user "Hello, can you assist me?"
end
ctx.talk(prompt)

Parameters:

  • b (Proc)

    A block that composes messages. If it takes one argument, it receives the prompt object. Otherwise it runs in prompt context.

Returns:



384
385
386
# File 'lib/llm/context.rb', line 384

def prompt(&b)
  LLM::Prompt.new(@llm, &b)
end

#remote_file(res) ⇒ LLM::Object

Reconginize an object as a remote file

Parameters:

Returns:



415
416
417
# File 'lib/llm/context.rb', line 415

def remote_file(res)
  LLM::Object.from(value: res, kind: :remote_file)
end

#returnsArray<LLM::Function::Return>

Returns tool returns accumulated in this context

Returns:



289
290
291
292
293
294
295
296
297
# File 'lib/llm/context.rb', line 289

def returns
  @messages
    .select(&:tool_return?)
    .flat_map do |msg|
      LLM::Function::Return === msg.content ?
        [msg.content] :
        [*msg.content].grep(LLM::Function::Return)
    end
end

#serialize(path:) ⇒ void Also known as: save

This method returns an undefined value.

Save the current context state

Examples:

llm = LLM.openai(key: ENV["KEY"])
ctx = LLM::Context.new(llm)
ctx.talk "Hello"
ctx.save(path: "context.json")

Raises:

  • (SystemCallError)

    Might raise a number of SystemCallError subclasses



475
476
477
# File 'lib/llm/context.rb', line 475

def serialize(path:)
  ::File.binwrite path, LLM.json.dump(to_h)
end

#spawn(function, strategy) ⇒ LLM::Function::Return, LLM::Function::Task

Spawns a function through the context.

When a guard is configured, this method can return an in-band guarded tool error instead of spawning work.

Parameters:

Returns:



280
281
282
283
284
# File 'lib/llm/context.rb', line 280

def spawn(function, strategy)
  warning = guard&.call(self)
  return guarded_return_for(function, warning) if warning
  function.spawn(strategy)
end

#streamLLM::Stream, ...

Returns a stream object, or nil

Returns:

  • (LLM::Stream, #<<, nil)

    Returns a stream object, or nil



437
438
439
# File 'lib/llm/context.rb', line 437

def stream
  @stream || @params[:stream]
end

#talk(prompt, params = {}) ⇒ LLM::Response

Interact with the context via the chat completions API. This method immediately sends a request to the LLM and returns the response.

Examples:

llm = LLM.openai(key: ENV["KEY"])
ctx = LLM::Context.new(llm)
res = ctx.talk("Hello, what is your name?")
puts res.messages[0].content

Parameters:

  • params (defaults to: {})

    The params, including optional :role (defaults to :user), :stream, :tools, :schema etc.

  • prompt (String)

    The input prompt to be completed

Returns:



198
199
200
201
202
203
204
205
206
207
208
209
# File 'lib/llm/context.rb', line 198

def talk(prompt, params = {})
  @owner = @llm.request_owner
  compactor.compact!(prompt) if compactor.compact?(prompt)
  repair!(@messages, prompt)
  prompt, params, res = mode == :responses ? respond(prompt, params) : complete(prompt, params)
  self.compacted = false
  role = params[:role] || @llm.user_role
  role = @llm.tool_role if params[:role].nil? && [*prompt].grep(LLM::Function::Return).any?
  @messages.concat LLM::Prompt === prompt ? prompt.to_a : [LLM::Message.new(role, prompt)]
  @messages.concat [res.choices[-1]].compact
  res
end

#to_hHash

Returns:

  • (Hash)


450
451
452
453
454
455
456
457
# File 'lib/llm/context.rb', line 450

def to_h
  {
    schema_version: 1,
    model:,
    compacted:,
    messages: @messages.map { serialize_message(_1) }
  }
end

#to_jsonString

Returns:

  • (String)


461
462
463
# File 'lib/llm/context.rb', line 461

def to_json(...)
  LLM.json.dump(to_h, ...)
end

#tracerLLM::Tracer

Returns an LLM tracer

Returns:



422
423
424
# File 'lib/llm/context.rb', line 422

def tracer
  @llm.tracer
end

#tracer=(other) ⇒ void

This method returns an undefined value.

Parameters:



430
431
432
# File 'lib/llm/context.rb', line 430

def tracer=(other)
  @llm.tracer = other || LLM::Tracer::Null.new(@llm)
end

#transformer#call?

Returns a transformer, if configured.

Transformers can rewrite outgoing prompts and params before a request is sent to the provider.

Returns:

  • (#call, nil)


171
172
173
# File 'lib/llm/context.rb', line 171

def transformer
  @transformer
end

#transformer=(transformer) ⇒ #call?

Sets a transformer.

Transformers must implement call(ctx, prompt, params) and return a two-element array of [prompt, params].

Parameters:

  • transformer (#call, nil)

Returns:

  • (#call, nil)


183
184
185
# File 'lib/llm/context.rb', line 183

def transformer=(transformer)
  @transformer = transformer
end

#usageLLM::Object

Returns token usage accumulated in this context

Returns:



351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/llm/context.rb', line 351

def usage
  if usage = @messages.find(&:assistant?)&.usage
    LLM::Object.from(
      input_tokens: usage.input_tokens || 0,
      output_tokens: usage.output_tokens || 0,
      reasoning_tokens: usage.reasoning_tokens || 0,
      input_audio_tokens: usage.input_audio_tokens || 0,
      output_audio_tokens: usage.output_audio_tokens || 0,
      input_image_tokens: usage.input_image_tokens || 0,
      cache_read_tokens: usage.cache_read_tokens || 0,
      cache_write_tokens: usage.cache_write_tokens || 0,
      total_tokens: usage.total_tokens || 0
    )
  else
    ZERO_USAGE
  end
end

#wait(strategy, except: []) ⇒ Array<LLM::Function::Return>

Waits for queued tool work to finish.

This prefers queued streamed tool work when the configured stream exposes a non-empty queue. Otherwise it falls back to waiting on the context's pending functions directly.

Parameters:

  • strategy (Symbol, Array<Symbol>)

    If the stream queue already has tool work, wait will drain it without using this argument. Otherwise, this controls how pending functions are resolved directly. Use :call for sequential execution without spawning.

  • except (Array<LLM::Function>) (defaults to: [])

    A list of functions to exclude from the wait

Returns:



314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/llm/context.rb', line 314

def wait(strategy, except: [])
  if stream.queue.empty?
    tools  = except.empty? ? functions : functions - except
    guards = guarded_returns(tools:)
    return guards if guards
    @queue = tools.spawn(strategy)
    returns = @queue.wait
    emit_tool_returns(tools, returns)
    returns
  else
    @queue = stream.queue
    @queue.wait
  end
ensure
  @queue = nil
  @stream = nil
end