Class: LLM::Context
- Inherits:
-
Object
- Object
- LLM::Context
- Includes:
- Deserializer, Serializer
- Defined in:
- lib/llm/context.rb,
lib/llm/context/serializer.rb,
lib/llm/context/deserializer.rb
Overview
LLM::Context is the low-level stateful execution boundary in llm.rb. Most users should start with Agent, which wraps Context and manages tool loops automatically. Use Context directly when you need manual control over tool execution.
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.
Defined Under Namespace
Modules: Deserializer, Serializer
Instance Attribute Summary collapse
-
#messages ⇒ LLM::Buffer<LLM::Message>
readonly
Returns the accumulated message history for this context.
-
#llm ⇒ LLM::Provider
readonly
Returns a provider.
-
#mode ⇒ Symbol
readonly
Returns the context mode.
-
#record ⇒ Object?
readonly
Returns the ORM record this context is bound to, or nil.
-
#id ⇒ String
readonly
Returns a stable id for this context.
-
#compacted ⇒ Boolean
(also: #compacted?)
private
Returns whether the context has been compacted and no later model response has cleared that state.
Class Method Summary collapse
-
.validate_schema!(version:)
Asserts that a payload can be restored by this runtime.
-
.params ⇒ Array<Symbol>
private
Returns the set of runtime parameters that configure this context and must never be forwarded to a provider request body.
-
.schema_version ⇒ Integer
Returns the newest payload version this runtime can restore, and the version it writes.
Instance Method Summary collapse
- #registry ⇒ LLM::Registry
-
#initialize(llm, params = {}) ⇒ Context
constructor
A new instance of Context.
-
#retry_budget ⇒ Integer
Returns the retry budget for rate-limited requests.
-
#params ⇒ Hash
Returns the default params for this context.
-
#compactor ⇒ LLM::Compactor
Returns a context compactor.
-
#compactor_options ⇒ Hash
Returns the options given to the context compactor.
- #intercept ⇒ LLM::Intercept
-
#filter ⇒ Class<LLM::MessageFilter>
Returns the configured filter class.
-
#talk(prompt, params = {}) ⇒ LLM::Response
Interact with the context via the chat completions API.
-
#ask(prompt, options = {}) {|String| ... } ⇒ LLM::Response
Ask a question and return the content string directly.
- #inspect ⇒ String
-
#pending_functions ⇒ Array<LLM::Function>
Returns an array of functions that can be called.
-
#pending_functions? ⇒ Boolean
Returns whether there is pending tool work in this context.
-
#spawn(function, strategy) ⇒ LLM::Function::Task
Spawns a function through the context.
-
#returns ⇒ Array<LLM::Function::Return>
Returns tool returns accumulated in this context.
-
#wait(strategy, except: []) ⇒ Array<LLM::Function::Return>
Waits for queued tool work to finish.
-
#interrupt! ⇒ nil
(also: #cancel!)
Interrupt a turn: the request in flight, the tools that are running, and - when it is neither of those - the caller the turn is running under.
-
#token_usage ⇒ LLM::Usage
(also: #usage)
Returns token usage accumulated in this context.
-
#context_used ⇒ Integer?
Returns the live context size (in tokens) of the most recent assistant message.
-
#context_usage ⇒ Rational?
Returns the fraction of the context window currently used.
-
#context_window ⇒ Integer?
Returns the model's context window.
-
#prompt(&b) ⇒ LLM::Prompt
(also: #build_prompt)
Build a role-aware prompt for a single request.
-
#image_url(url) ⇒ LLM::Object
Recongize an object as a URL to an image.
-
#local_file(path) ⇒ LLM::Object
Recongize an object as a local file.
-
#remote_file(res) ⇒ LLM::Object
Reconginize an object as a remote file.
-
#tracer ⇒ LLM::Tracer
Returns an LLM tracer.
- #tracer=(other)
-
#stream ⇒ LLM::Stream, ...
Returns a stream object, or nil.
-
#model ⇒ String
Returns the model a Context is actively using.
-
#created_at ⇒ Time?
Returns the time this context was created, derived from the timestamp embedded in its UUIDv7 id, or nil when it is not.
-
#to_h ⇒ Hash
Returns the runtime state as a Hash.
- #to_json ⇒ String
-
#serialize(path:)
(also: #save)
Save the current context state.
-
#cost ⇒ LLM::Cost
Returns an approximate cost for a given context based on both the provider, and model.
Methods included from Deserializer
Constructor Details
#initialize(llm, params = {}) ⇒ Context
Returns a new instance of Context.
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 |
# File 'lib/llm/context.rb', line 144 def initialize(llm, params = {}) params = {}.merge!(params) @llm = llm @record = params.delete(:record) @id = params.delete(:id) || makeid raise LLM::Error, "an id must be a UUIDv7 string" unless LLM::Utils.uuidv7?(@id) @mode = params.delete(:mode) || (llm.name == :openai ? :responses : :completions) tools = [*params.delete(:tools)] @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:) @compactor = { klass: params.delete(:compactor) || LLM::Compactor::Null, options: params.delete(:compactor_options) || {} } @filter = { klass: params.delete(:filter) || LLM::MessageFilter::Null, options: params.delete(:filter_options) || {} } @intercept = { klass: params.delete(:intercept) || LLM::Intercept::Null, options: params.delete(:intercept_options) || {} } @retry_budget = params.delete(:retry_budget) || 0 end |
Instance Attribute Details
#messages ⇒ LLM::Buffer<LLM::Message> (readonly)
Returns the accumulated message history for this context
88 89 90 |
# File 'lib/llm/context.rb', line 88 def @messages end |
#llm ⇒ LLM::Provider (readonly)
Returns a provider
93 94 95 |
# File 'lib/llm/context.rb', line 93 def llm @llm end |
#mode ⇒ Symbol (readonly)
Returns the context mode
98 99 100 |
# File 'lib/llm/context.rb', line 98 def mode @mode end |
#record ⇒ Object? (readonly)
Returns the ORM record this context is bound to, or nil.
103 104 105 |
# File 'lib/llm/context.rb', line 103 def record @record end |
#id ⇒ String (readonly)
Returns a stable id for this context. It is generated once on creation and restored with the runtime state on load - a context bound to a record takes the record's id instead, when that id is a UUIDv7 string - and it is always a UUIDv7 string.
111 112 113 |
# File 'lib/llm/context.rb', line 111 def id @id end |
#compacted ⇒ Boolean 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.
206 207 208 |
# File 'lib/llm/context.rb', line 206 def compacted @compacted end |
Class Method Details
.validate_schema!(version:)
This method returns an undefined value.
Asserts that a payload can be restored by this runtime.
A payload is readable when it carries a version no newer than this runtime writes, so a payload from version 1 is still readable. A payload written by a newer runtime is refused, and one that carries no version at all is refused too.
68 69 70 71 72 73 |
# File 'lib/llm/context.rb', line 68 def self.validate_schema!(version:) return if version and version <= schema_version raise LLM::Error, "the payload was written by another version of " \ "llm.rb (schema_version #{version.inspect}, this " \ "runtime reads up to #{schema_version})" end |
.params ⇒ Array<Symbol>
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 the set of runtime parameters that configure this context and must never be forwarded to a provider request body.
81 82 83 |
# File 'lib/llm/context.rb', line 81 def self.params %w[intercept retry_budget concurrency filter compactor record id] end |
.schema_version ⇒ Integer
Returns the newest payload version this runtime can restore, and the version it writes.
A payload written by a newer runtime is refused, because a field this runtime does not know about would be dropped silently.
51 52 53 |
# File 'lib/llm/context.rb', line 51 def self.schema_version 2 end |
Instance Method Details
#registry ⇒ LLM::Registry
622 623 624 |
# File 'lib/llm/context.rb', line 622 def registry llm.registry end |
#retry_budget ⇒ Integer
Returns the retry budget for rate-limited requests.
176 177 178 |
# File 'lib/llm/context.rb', line 176 def retry_budget @retry_budget end |
#params ⇒ Hash
Returns the default params for this context
183 184 185 |
# File 'lib/llm/context.rb', line 183 def params @params.dup end |
#compactor ⇒ LLM::Compactor
Returns a context compactor
190 191 192 |
# File 'lib/llm/context.rb', line 190 def compactor @compactor[:klass] end |
#compactor_options ⇒ Hash
Returns the options given to the context compactor
197 198 199 |
# File 'lib/llm/context.rb', line 197 def @compactor[:options].dup end |
#intercept ⇒ LLM::Intercept
212 213 214 215 216 217 218 219 |
# File 'lib/llm/context.rb', line 212 def intercept klass = @intercept[:klass] intercept = Class === klass ? klass.new(llm) : klass intercept.tap do it.instance_variable_set(:@context, self) it.instance_variable_set(:@options, @intercept[:options]) end end |
#filter ⇒ Class<LLM::MessageFilter>
Returns the configured filter class.
Filters rewrite the most recent message before it is sent to the provider.
228 229 230 |
# File 'lib/llm/context.rb', line 228 def filter @filter[:klass] 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.
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 |
# File 'lib/llm/context.rb', line 243 def talk(prompt, params = {}) @owner = @llm.request_owner @compactor[:klass].new(self).call(**@compactor[:options]) repair!(@messages, prompt) prompt, params, res = try { mode == :responses ? respond(prompt, params) : complete(prompt, params) } self.compacted = false if prompt.all?(&:tool_return?) @messages.concat prompt.map { LLM::Message.new(@llm.tool_role, _1.content, _1.extra) } else @messages.concat(prompt) end @messages.concat([res.choices[-1]].compact) stream.on_step(self, res) res ensure @owner = nil end |
#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,
the way #talk does.
274 275 276 277 278 279 280 281 282 283 284 285 286 |
# File 'lib/llm/context.rb', line 274 def ask(prompt, = {}, &block) = {with: nil, stream: nil}.merge!( || {}) with, stream = .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 |
#inspect ⇒ String
290 291 292 293 294 |
# File 'lib/llm/context.rb', line 290 def inspect "#<#{LLM::Utils.object_id(self)} " \ "@llm=#{@llm.class}, @mode=#{@mode.inspect}, @params=#{@params.inspect}, " \ "@messages=#{@messages.inspect}>" end |
#pending_functions ⇒ Array<LLM::Function>
Returns an array of functions that can be called
299 300 301 302 303 304 305 306 307 308 309 310 311 |
# File 'lib/llm/context.rb', line 299 def pending_functions return_ids = returns.map(&:id) @messages .select(&:assistant?) .flat_map do |msg| fns = msg.functions.select { _1.pending? and !return_ids.include?(_1.id) } fns.each do |fn| fn.tracer = tracer fn.model = msg.model fn.intercept = intercept end end.extend(LLM::Function::Array) end |
#pending_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.
318 319 320 321 |
# File 'lib/llm/context.rb', line 318 def pending_functions? pending = queue (pending && !pending.empty?) || pending_functions.any? end |
#spawn(function, strategy) ⇒ LLM::Function::Task
Spawns a function through the context.
329 330 331 |
# File 'lib/llm/context.rb', line 329 def spawn(function, strategy) function.task(strategy) end |
#returns ⇒ Array<LLM::Function::Return>
Returns tool returns accumulated in this context
336 337 338 339 340 341 342 343 344 |
# File 'lib/llm/context.rb', line 336 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 |
#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.
361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 |
# File 'lib/llm/context.rb', line 361 def wait(strategy, except: []) if stream.queue.empty? ## # Every pending function is spawned as a task that checks its own # intercept (stamped on the function) before running. Blocked tasks # yield their intercept's return, so all pending calls still close. tools = except.empty? ? pending_functions : pending_functions - except @queue = tools.task(strategy) returns = @queue.wait emit_tool_returns(tools, returns) returns else @queue = stream.queue @queue.wait end rescue LLM::Interrupt ## # Once, not once per tool. A cancel reaches every tool that is # running, and the caller hears one exception, so the phase is # announced here - the frame every strategy's interrupt unwinds # through - and before the caller is given it. tracer.on_interrupt(scope: :tool) raise ensure @queue = nil @stream = nil end |
#interrupt! ⇒ nil Also known as: cancel!
Interrupt a turn: the request in flight, the tools that are running, and - when it is neither of those - the caller the turn is running under.
This is inspired by Go's context cancellation model, and the three
are the whole of what a turn is doing. A request is closed at the
socket, which is llm.interrupt!(owner): the transport keys its
live requests by the owner the request was made on, and answers
that it has nothing to close when there is no request registered
under it. A tool is a task, and every task that is running is
raised into. Between the two there is nothing: the loop is between
two requests, or waiting out a retry, or building the next one -
and until Agent#run_loop recorded the caller it is running
under, an interrupt there was a cancel that did nothing at all.
The caller is the last resort rather than the first: the two precise
interrupts are what close a socket and stop a tool, and what is left
over is a turn that is running with nothing to point at. Ending that
is what the caller is asked to do - it answers interrupt!, announces
the phase to the tracer, and which of the thread, the fiber and the
scheduler it holds receives the raise is its own business. See
Agent::Interrupt.
413 414 415 416 417 418 419 420 421 |
# File 'lib/llm/context.rb', line 413 def interrupt! llm.interrupt!(@owner) queue&.interrupt! pending_functions.each(&:interrupt!) @queue = nil @owner = nil @caller&.interrupt! nil end |
#token_usage ⇒ LLM::Usage Also known as: usage
Returns token usage accumulated in this context
427 428 429 430 431 432 433 |
# File 'lib/llm/context.rb', line 427 def token_usage @messages .select(&:assistant?) .map(&:token_usage) .compact .reduce(LLM::Usage.zero, :+) end |
#context_used ⇒ Integer?
Returns the live context size (in tokens) of the most recent assistant message.
440 441 442 443 444 445 |
# File 'lib/llm/context.rb', line 440 def context_used @messages .find(&:assistant?) &.token_usage &.total_tokens end |
#context_usage ⇒ Rational?
Returns the fraction of the context window currently used. For example: Rational(100, 10_000), or nil when unknown
451 452 453 454 455 456 457 458 459 |
# File 'lib/llm/context.rb', line 451 def context_usage return nil if @messages.size < 2 used = context_used return nil if used.nil? total = context_window total.nil? || total <= 0 ? nil : Rational(used, total) rescue LLM::NoSuchModelError, LLM::NoSuchRegistryError nil end |
#context_window ⇒ Integer?
This method returns nil when the context window size is not known to the runtime
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.
469 470 471 472 473 474 475 |
# File 'lib/llm/context.rb', line 469 def context_window registry .limit(model:) .context rescue LLM::NoSuchModelError, LLM::NoSuchRegistryError nil 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.
492 493 494 |
# File 'lib/llm/context.rb', line 492 def prompt(&b) LLM::Prompt.new(@llm, &b) end |
#image_url(url) ⇒ LLM::Object
Recongize an object as a URL to an image
503 504 505 |
# File 'lib/llm/context.rb', line 503 def image_url(url) LLM::Object.from(value: url, kind: :image_url) end |
#local_file(path) ⇒ LLM::Object
Recongize an object as a local file
513 514 515 |
# File 'lib/llm/context.rb', line 513 def local_file(path) LLM::Object.from(value: LLM.File(path), kind: :local_file) end |
#remote_file(res) ⇒ LLM::Object
Reconginize an object as a remote file
523 524 525 |
# File 'lib/llm/context.rb', line 523 def remote_file(res) LLM::Object.from(value: res, kind: :remote_file) end |
#tracer ⇒ LLM::Tracer
Returns an LLM tracer
530 531 532 |
# File 'lib/llm/context.rb', line 530 def tracer @llm.tracer end |
#tracer=(other)
This method returns an undefined value.
538 539 540 |
# File 'lib/llm/context.rb', line 538 def tracer=(other) @llm.tracer = other || LLM::Tracer::Null.new(@llm) end |
#stream ⇒ LLM::Stream, ...
Returns a stream object, or nil
545 546 547 |
# File 'lib/llm/context.rb', line 545 def stream @stream || @params[:stream] end |
#model ⇒ String
Returns the model a Context is actively using
552 553 554 |
# File 'lib/llm/context.rb', line 552 def model .find(&:assistant?)&.model || @params[:model] || @llm.default_model end |
#created_at ⇒ Time?
Returns the time this context was created, derived from the timestamp embedded in its UUIDv7 id, or nil when it is not.
560 561 562 |
# File 'lib/llm/context.rb', line 560 def created_at @created_at ||= LLM::Utils.(@id) end |
#to_h ⇒ Hash
Returns the runtime state as a Hash.
context_used and context_window are projections written for
queryability, so that saved state can be inspected at rest
without loading it. They are never read back; the runtime
always derives them from the messages and the registry.
Version 2 saves tool calls in the abstract form only. A version 1 payload carried the provider's wire form beside it, and that field is dropped on the way in - an adapter rebuilds its own blocks from the abstract form anyway.
577 578 579 580 581 582 583 584 585 586 587 588 |
# File 'lib/llm/context.rb', line 577 def to_h { schema_version: LLM::Context.schema_version, id: @id, model:, compacted:, context_used:, context_window:, token_usage: token_usage.to_h, messages: @messages.map { (_1) } } end |
#to_json ⇒ String
592 593 594 |
# File 'lib/llm/context.rb', line 592 def to_json(...) LLM.json.dump(to_h, ...) end |