Class: LittleGhost::Agent

Inherits:
Object
  • Object
show all
Extended by:
Support::ClassAttributes
Includes:
ContextManagement, Delegation, Skills, ToolLoop
Defined in:
lib/little_ghost/agent.rb,
lib/little_ghost/agent/skills.rb,
lib/little_ghost/agent/tool_loop.rb,
lib/little_ghost/agent/delegation.rb,
lib/little_ghost/agent/context_management.rb

Overview

Define reusable agents that can answer, stream, call tools, and delegate work. Each subclass describes one application role with an inheritable Ruby DSL.

A customer support agent can look up an account itself and give longer investigations to a research specialist:

class ResearchAgent < LittleGhost::Agent
description "Researches transfer failures"
model "customer_support.research"
tools LedgerSearchTool
end

class CustomerSupportAgent < LittleGhost::Agent
description "Handles support requests"
model :customer_support
limits max_turns: 40
tools AccountLookupTool
subagent ResearchAgent, kind: "research"
end

run = CustomerSupportAgent.ask("Why is transfer 481 pending?")
run.completed? # => true
run.response   # => "Transfer 481 is waiting for the receiving bank."

Class declarations are inherited. Prompts resolve by the agent's logical path unless system_prompt or system_template supplies one explicitly; tools and prompt locals may also be selected dynamically for each run. Capabilities such as skills, context management, loop detection, and delegation remain inactive until their DSL methods are called.

The class-level ask and stream_ask helpers create fresh standalone entrypoints. Create an instance explicitly to reuse one Runtime across calls. Runtimes build bound instances internally; their call method returns a RunResult and their stream method follows the owning run's single-execution lifecycle. Closing an agent closes owned tools and any standalone workspace and sandbox. LittleGhost::Agent.ask uses You are a helpful agent. as its system prompt. Subclasses continue to use their inline or conventional prompts.

Models can return ordinary text or a locally validated structured result. Tool failures are sanitized before returning to the model, diagnostic capture can be disabled for sensitive agents, and cancellation, deadlines, and cleanup failures remain framework control flow.

Defined Under Namespace

Modules: ContextManagement, Delegation, Skills, ToolLoop

Constant Summary collapse

DEFAULT_SYSTEM_PROMPT =

:nodoc:

"You are a helpful agent."
DEFAULT_MAX_TOOL_RESULT_TOKENS =

:nodoc:

10_000
MAX_STRUCTURED_RESULT_BYTES =

:nodoc:

1_000_000
MAX_STRUCTURED_RESULT_DEPTH =

:nodoc:

64
MAX_STRUCTURED_RESULT_NODES =

:nodoc:

100_000
RESULT_SCHEMA_KEYWORDS =
%w[
  $schema title description type enum minimum maximum minLength maxLength
  properties required additionalProperties minItems maxItems items
].freeze
CALLBACKS =

:nodoc:

%i[
  after_initialize
  before_invocation after_invocation
  before_model after_model after_model_error
  before_tool after_tool
].freeze

Constants included from ContextManagement

ContextManagement::DEFAULT_COMPRESSION_THRESHOLD, ContextManagement::DEFAULT_CONTEXT_WINDOW_TOKENS, ContextManagement::DEFAULT_PRESERVE_RECENT_MESSAGES, ContextManagement::DEFAULT_SUMMARY_RATIO, ContextManagement::ESTIMATED_CHARS_PER_TOKEN, ContextManagement::OUTPUT_LIMIT_STOP_REASONS, ContextManagement::SUMMARIZATION_PROMPT

Constants included from ToolLoop

ToolLoop::FINAL_WARNING, ToolLoop::TRACKED_INVOCATION_LIMIT, ToolLoop::WARNING

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Support::ClassAttributes

class_attribute, included

Methods included from Delegation

included

Methods included from Skills

included

Methods included from ContextManagement

included

Methods included from ToolLoop

included

Constructor Details

#initialize(model: nil, runtime: nil, tools: [], template_resolver: nil, template_paths: [], run: nil, executor: Support::Executor.new, delegation_activity: nil, agent_path: Subagents::AgentPath::ROOT, max_turns: 100, max_tool_calls: 1_000, max_tool_result_tokens: DEFAULT_MAX_TOOL_RESULT_TOKENS, model_settings: {}, workspace: nil, sandbox: nil) ⇒ Agent

Creates either a standalone entrypoint or a run-scoped agent.

Calling new without model and run creates the standalone form used by ask and stream_ask. Runtime builders supply the remaining dependencies and apply class-level limits and declarations.



512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
# File 'lib/little_ghost/agent.rb', line 512

def initialize(
  model: nil,
  runtime: nil,
  tools: [],
  template_resolver: nil,
  template_paths: [],
  run: nil,
  executor: Support::Executor.new,
  delegation_activity: nil,
  agent_path: Subagents::AgentPath::ROOT,
  max_turns: 100,
  max_tool_calls: 1_000,
  max_tool_result_tokens: DEFAULT_MAX_TOOL_RESULT_TOKENS,
  model_settings: {},
  workspace: nil,
  sandbox: nil
)
  if model.nil? && run.nil?
    @standalone = true
    @runtime = runtime || Runtime.new(configuration: LittleGhost.configuration)
    @workspace = workspace
    @sandbox = sandbox
    @owns_resources = true
    @closed = false
    @close_mutex = Mutex.new
    @interruptions_mutex = Mutex.new
    @active_interruptions = []
    return
  end

  @model = model
  @runtime = runtime || run&.runtime
  @run = run
  @workspace = workspace || run&.workspace
  @sandbox = sandbox || run&.sandbox
  if @runtime.is_a?(Runtime) && !@workspace
    @workspace = @runtime.build_workspace
    @sandbox ||= @runtime.build_sandbox(workspace: @workspace)
  end
  @owns_resources = run.nil? && (@workspace || @sandbox)
  binding = Tool::Binding.new(agent: self, run:, runtime: @runtime, model:, workspace: @workspace, sandbox: @sandbox)
  @tool_registry = ToolRegistry.new(tools, binding:)
  self.class.tool_declarations.each do |declaration|
    @tool_registry.register(declaration, replace: true)
  end
  @structured_output_strategy = StructuredOutput.resolve(
    self.class.result_schema,
    model:,
    ordinary_tools: @tool_registry.specifications
  )
  @model_settings = model_settings.to_h.freeze
  @template_resolver = template_resolver || default_template_resolver(template_paths)
  @executor = executor
  @delegation_activity = delegation_activity
  @agent_path = Subagents::AgentPath.validate!(agent_path)
  @max_turns = Integer(max_turns)
  @max_tool_calls = Integer(max_tool_calls)
  @max_tool_result_tokens = Integer(max_tool_result_tokens)
  @closed = false
  @close_mutex = Mutex.new
  @exclusive_tools_mutex = Mutex.new
  @interruptions_mutex = Mutex.new
  @active_interruptions = []
  raise ArgumentError, "max_turns must be at least 1" if @max_turns < 1
  raise ArgumentError, "max_tool_calls must be at least 1" if @max_tool_calls < 1
  raise ArgumentError, "max_tool_result_tokens must be at least 1" if @max_tool_result_tokens < 1
  apply_cancellation_decision!(run_callbacks(:after_initialize, self))
rescue
  @tool_registry&.close
  raise
end

Instance Attribute Details

#agent_pathObject (readonly)

Run-scoped model, tools, lifecycle, delegation, and execution resources available to agent extensions.



504
505
506
# File 'lib/little_ghost/agent.rb', line 504

def agent_path
  @agent_path
end

#delegation_activityObject (readonly)

Run-scoped model, tools, lifecycle, delegation, and execution resources available to agent extensions.



504
505
506
# File 'lib/little_ghost/agent.rb', line 504

def delegation_activity
  @delegation_activity
end

#max_tool_callsObject (readonly)

Run-scoped model, tools, lifecycle, delegation, and execution resources available to agent extensions.



504
505
506
# File 'lib/little_ghost/agent.rb', line 504

def max_tool_calls
  @max_tool_calls
end

#modelObject (readonly)

Run-scoped model, tools, lifecycle, delegation, and execution resources available to agent extensions.



504
505
506
# File 'lib/little_ghost/agent.rb', line 504

def model
  @model
end

#runObject (readonly)

Run-scoped model, tools, lifecycle, delegation, and execution resources available to agent extensions.



504
505
506
# File 'lib/little_ghost/agent.rb', line 504

def run
  @run
end

#runtimeObject (readonly)

Runtime used to build this agent's model, tools, workspace, and sandbox.



585
586
587
# File 'lib/little_ghost/agent.rb', line 585

def runtime
  @runtime
end

#sandboxObject (readonly)

Run-scoped model, tools, lifecycle, delegation, and execution resources available to agent extensions.



504
505
506
# File 'lib/little_ghost/agent.rb', line 504

def sandbox
  @sandbox
end

#tool_registryObject (readonly)

Run-scoped model, tools, lifecycle, delegation, and execution resources available to agent extensions.



504
505
506
# File 'lib/little_ghost/agent.rb', line 504

def tool_registry
  @tool_registry
end

#workspaceObject (readonly)

Run-scoped model, tools, lifecycle, delegation, and execution resources available to agent extensions.



504
505
506
# File 'lib/little_ghost/agent.rb', line 504

def workspace
  @workspace
end

Class Method Details

.agent_id(*values) ⇒ Object

:call-seq:

agent_id() -> String
agent_id(value) -> String

The stable identifier used in telemetry, delegation, and default tool names. Named subclasses derive it from their underscored class name without an Agent suffix; passing value replaces that default.



112
113
114
115
116
# File 'lib/little_ghost/agent.rb', line 112

def agent_id(*values)
  return agent_id_value || default_agent_id if values.empty?

  self.agent_id_value = values.fetch(0).to_s
end

.ask(message, **options) ⇒ Object

Executes message through a fresh standalone entrypoint and returns the completed LittleGhost::Run. Invocation options are forwarded to #ask.

Create an instance explicitly when reusing a Runtime or streaming events.



88
89
90
# File 'lib/little_ghost/agent.rb', line 88

def ask(message, **options)
  new.ask(message, **options)
end

.callbacksObject

:nodoc:



306
# File 'lib/little_ghost/agent.rb', line 306

def callbacks = callback_values # :nodoc:

.capture_diagnostics(*values) ⇒ Object

:call-seq:

capture_diagnostics() -> true or false
capture_diagnostics(value) -> true or false

Whether agent-layer diagnostics may include model and tool content.

Capture defaults to true, and only a literal true enables it. This setting does not disable run-level input and output capture from an enabled process-wide Support::ContentCapture policy. For sensitive work, also install Support::ContentCapture.disabled or an appropriate scrubber through Instrumentation.capture_content.



240
241
242
243
244
# File 'lib/little_ghost/agent.rb', line 240

def capture_diagnostics(*values)
  return capture_diagnostics_value if values.empty?

  self.capture_diagnostics_value = values.fetch(0) == true
end

.description(*values) ⇒ Object

:call-seq:

description() -> String
description(value) -> String

The human-readable description shown when this agent is delegated.



130
131
132
133
134
# File 'lib/little_ghost/agent.rb', line 130

def description(*values)
  return description_value.to_s if values.empty?

  self.description_value = values.fetch(0).to_s
end

.limits(**values) ⇒ Object

:call-seq:

limits() -> Hash
limits(**values) -> Hash

Inherited execution limits for model turns, tool calls, and tool output.

Keyword arguments merge into the current limits and the zero-argument form returns them.



176
177
178
179
180
# File 'lib/little_ghost/agent.rb', line 176

def limits(**values)
  return limits_value if values.empty?

  self.limits_value = limits.merge(values.transform_keys(&:to_sym))
end

.logical_pathObject

The underscored, namespace-aware path used for conventional prompt lookup.



119
120
121
122
123
# File 'lib/little_ghost/agent.rb', line 119

def logical_path
  parts = name.to_s.split("::")
  parts[-1] = parts.last.sub(/Agent\z/, "") if parts.any?
  parts.reject(&:empty?).map { |part| underscore(part) }.join("/")
end

.model(*values, &block) ⇒ Object

:call-seq:

model() -> String, Symbol, Hash, Proc, nil
model(role_or_target) -> String, Symbol
model(provider:, model:, **settings) -> Hash
model { |invocation| ... } -> Proc

Selects this agent's model by logical role, canonical provider:model-id target, or an inline mapping with provider, model, and trusted model settings. The provider names a configured connection, not necessarily its adapter.

Pass a block to choose any supported form from each Invocation at run time. Inline mappings use flat settings, for example:

model(provider: "openai", model: "gpt-5.6-luna", reasoning_effort: "high")


151
152
153
154
155
156
157
158
159
160
161
# File 'lib/little_ghost/agent.rb', line 151

def model(*values, &block)
  return model_value if values.empty? && !block
  if block && !values.empty?
    raise ArgumentError, "model accepts either one selection or a block"
  end
  if values.length > 1
    raise ArgumentError, "model accepts one selection"
  end

  self.model_value = block || copy_model_selection(values.fetch(0))
end

.model_selection(invocation) ⇒ Object

:nodoc:



163
164
165
166
# File 'lib/little_ghost/agent.rb', line 163

def model_selection(invocation) # :nodoc:
  value = model_value
  value.is_a?(Proc) ? value.call(invocation) : value
end

.prompt_local(name, *values, &resolver) ⇒ Object

Adds a named value or resolver to every prompt rendered for the agent.

Raises:

  • (ArgumentError)


297
298
299
300
301
302
# File 'lib/little_ghost/agent.rb', line 297

def prompt_local(name, *values, &resolver)
  raise ArgumentError, "Provide a prompt local value or block" if values.empty? && !resolver
  raise ArgumentError, "Provide a prompt local value or block, not both" unless values.empty? || !resolver

  self.prompt_local_values = prompt_local_values.merge(name.to_sym => resolver || values.fetch(0))
end

.prompt_local_resolversObject

:nodoc:



304
# File 'lib/little_ghost/agent.rb', line 304

def prompt_local_resolvers = prompt_local_values # :nodoc:

.result_schema(schema = nil, name: nil, description: nil, strategy: :auto, **schema_keywords) ⇒ Object

:call-seq:

result_schema() -> Hash, nil
result_schema(schema, name: nil, description: nil, strategy: :auto) -> Hash
result_schema(name: nil, description: nil, strategy: :auto, **schema) -> Hash

Declares a strict JSON-object result contract. Every object must set additionalProperties: false and require each property. Automatic strategy selection prefers provider-native structured output and falls back to a terminal tool when supported.

A missing or invalid result receives one repair attempt before LittleGhost::StructuredResultError is raised. Invalid schemas and strategies raise LittleGhost::ConfigurationError immediately.

Raises:

  • (ArgumentError)


195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/little_ghost/agent.rb', line 195

def result_schema(schema = nil, name: nil, description: nil, strategy: :auto, **schema_keywords)
  return result_schema_value if schema.nil? && schema_keywords.empty? && name.nil? && description.nil? && strategy == :auto

  if schema.nil?
    schema = schema_keywords
  elsif !schema_keywords.empty?
    raise ArgumentError, "Provide result_schema as a hash or keyword schema, not both"
  end

  raise ArgumentError, "result_schema must be a hash" unless schema.is_a?(Hash)

  normalized_schema = Class.new(Tool).tap { |tool| tool.input_schema(schema) }.input_schema
  validate_result_schema_keywords!(normalized_schema)
  unless normalized_schema["type"] == "object"
    raise ConfigurationError, "result_schema must describe a top-level object"
  end

  schema_name = (name || "#{agent_id}_result").to_s
  unless schema_name.match?(/\A[a-zA-Z0-9_-]{1,64}\z/)
    raise ConfigurationError, "result_schema name must contain 1-64 letters, numbers, underscores, or hyphens"
  end
  strategy = strategy.to_sym
  unless StructuredOutput::STRATEGIES.include?(strategy)
    raise ConfigurationError, "result_schema strategy must be auto, provider, or tool"
  end

  self.result_schema_value = {
    schema: normalized_schema,
    name: schema_name,
    description: description&.to_s,
    strategy:
  }
end

.stream_ask(message, **options) ⇒ Object

Streams message through a fresh standalone entrypoint and returns an Enumerator of LittleGhost::StreamEvent objects. Invocation options are forwarded to #stream_ask.

Create an instance explicitly when reusing one Runtime across calls.



97
98
99
100
101
102
103
# File 'lib/little_ghost/agent.rb', line 97

def stream_ask(message, **options)
  stream = nil
  Enumerator.new do |events|
    stream ||= new.stream_ask(message, **options)
    stream.each { |event| events << event }
  end
end

.system_prompt(*values, &block) ⇒ Object

:call-seq:

system_prompt() -> String, Proc, nil
system_prompt(value) -> String
system_prompt { |locals| ... } -> Proc

The inline system prompt or prompt-building block.

Setting an inline prompt clears system_template so one source remains authoritative.



266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/little_ghost/agent.rb', line 266

def system_prompt(*values, &block)
  return system_prompt_builder_value || system_prompt_value if values.empty? && !block

  self.system_template_value = nil
  if block
    self.system_prompt_value = nil
    self.system_prompt_builder_value = block
  else
    self.system_prompt_value = values.fetch(0).to_s
    self.system_prompt_builder_value = nil
  end
end

.system_template(*values) ⇒ Object

:call-seq:

system_template() -> String, nil
system_template(path) -> String

The explicit system prompt template path, when conventional lookup is not used.



251
252
253
254
255
# File 'lib/little_ghost/agent.rb', line 251

def system_template(*values)
  return system_template_value if values.empty?

  self.system_template_value = values.fetch(0).to_s
end

.tool_declarationsObject

:nodoc:



294
# File 'lib/little_ghost/agent.rb', line 294

def tool_declarations = tool_declarations_value # :nodoc:

.tools(*values) ⇒ Object

Adds tool or provider classes to the agent.

Every declaration must be a class. A provider class can supply tools dynamically by implementing tools(binding).



283
284
285
286
287
288
289
290
291
292
# File 'lib/little_ghost/agent.rb', line 283

def tools(*values)
  invalid = values.flatten.compact.find { |value| !value.is_a?(Class) }
  if invalid
    raise ConfigurationError, "Class-level tools must be classes"
  end

  declarations = tool_declarations_value + values
  self.tool_declarations_value = declarations
  tool_declarations
end

Instance Method Details

#as_tool(name: self.class.agent_id, description: self.class.description, preserve_context: false) ⇒ Object

Exposes this agent as a Tool instance.

By default, each call starts with empty conversational history. Set preserve_context: true to retain history serially between calls.



824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
# File 'lib/little_ghost/agent.rb', line 824

def as_tool(name: self.class.agent_id, description: self.class.description, preserve_context: false)
  agent = self
  description = "Delegate a task to #{name}." if description.to_s.empty?
  mutex = Mutex.new
  retained_history = []
  tool_class = Tool.define(
    name: name,
    description: description,
    input_schema: {
      type: "object",
      properties: {input: {type: "string"}},
      required: ["input"],
      additionalProperties: false
    }
  ) do |input, context: nil|
    invocation = lambda do
      result = agent.call(
        input.fetch("input"),
        history: preserve_context ? retained_history : [],
        context: context&.state || {},
        cancellation_token: context&.cancellation_token || Support::CancellationToken.new,
        interruption_metadata: context&.,
        interruption_ids: context&.interruption_ids || [],
        deadline: context&.deadline,
        parent_operation_id: run&.operation_id
      )
      retained_history.replace(result.messages.reject { |message| message.role == :system }) if preserve_context
      result.structured? ? result.structured_result.value : result.text
    end
    preserve_context ? mutex.synchronize(&invocation) : invocation.call
  end
  tool_class.define_method(:close) { agent.close }
  binding = Tool::Binding.new(
    agent: self,
    run:,
    runtime:,
    model:,
    workspace:,
    sandbox:
  )
  tool_class.new(binding:)
end

#ask(message, **options) ⇒ Object

Runs message to completion through the standalone or run-scoped agent.



618
619
620
# File 'lib/little_ghost/agent.rb', line 618

def ask(message, **options)
  call(message, **options)
end

#build_run(payload) ⇒ Object

:nodoc:



593
594
595
596
597
598
599
600
601
# File 'lib/little_ghost/agent.rb', line 593

def build_run(payload) # :nodoc:
  options = {
    agent_class: self.class,
    entrypoint_class: self.class
  }
  options[:workspace] = workspace if workspace
  options[:sandbox] = sandbox if sandbox
  runtime.build_run(payload, **options)
end

#call(input = nil, **options) ⇒ Object

Runs input to completion.

A standalone agent returns a LittleGhost::Run. An agent built inside a run returns its LittleGhost::RunResult.



607
608
609
610
611
612
613
614
615
# File 'lib/little_ghost/agent.rb', line 607

def call(input = nil, **options)
  return build_run(entrypoint_payload(input, options)).call if @standalone

  result = nil
  stream(input, **options).each do |event|
    result = event.data[:result] if event.type == :invocation_stop
  end
  result
end

#closeObject

Closes owned tools, interruptions, sandbox, and workspace resources. The operation is idempotent and re-raises the first cleanup failure.



884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
# File 'lib/little_ghost/agent.rb', line 884

def close
  resources, interruptions = @close_mutex.synchronize do
    return if @closed

    @closed = true
    [
      [tool_registry, (@sandbox if @owns_resources), (@workspace if @owns_resources)],
      @interruptions_mutex.synchronize { @active_interruptions.dup }
    ]
  end
  first_error = nil
  interruptions.each do |active|
    active.close(AgentInterruptError.new("Agent was closed"))
  end
  resources.each do |resource|
    resource.close if resource.respond_to?(:close)
  rescue => error
    first_error ||= error
  end
  raise first_error if first_error
end

#dispatch_tools(tool_uses, context:, events:, parent_operation_id:, parent_trace_context: nil) ⇒ Object

:nodoc:



589
590
591
# File 'lib/little_ghost/agent.rb', line 589

def dispatch_tools(tool_uses, context:, events:, parent_operation_id:, parent_trace_context: nil) # :nodoc:
  execute_tools(tool_uses, context, events, parent_operation_id:, parent_trace_context:)
end

#entrypoint_nameObject

:nodoc:



587
# File 'lib/little_ghost/agent.rb', line 587

def entrypoint_name = self.class.agent_id # :nodoc:

#interrupt(message, cancellation_token: Support::CancellationToken.new, deadline: nil, target_operation_id: nil, interruption_id: nil, batch_key: nil, metadata: {}) ⇒ Object

Adds message to one active invocation and waits for its ordinary text reply.

The response may continue into tool calls; delivery does not stop the original invocation. Raises LittleGhost::AgentInterruptError when there is no unambiguous active target.



627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
# File 'lib/little_ghost/agent.rb', line 627

def interrupt(
  message,
  cancellation_token: Support::CancellationToken.new,
  deadline: nil,
  target_operation_id: nil,
  interruption_id: nil,
  batch_key: nil,
  metadata: {}
)
  interrupt_response(
    message,
    cancellation_token:,
    deadline:,
    target_operation_id:,
    interruption_id:,
    batch_key:,
    metadata:
  ).text
end

#interrupt_response(message, cancellation_token: Support::CancellationToken.new, deadline: nil, target_operation_id: nil, interruption_id: nil, batch_key: nil, metadata: {}) ⇒ Object

Adds an interruption and returns the model's immediate response details.

Use target_operation_id when an agent has multiple active invocations. Messages may contain only text, image, or document content. The returned response value exposes text, tool_calls?, interruption_ids, and batch_key; tool calls may continue after this response. Depend on these methods rather than the response's concrete class.

Raises:

  • (ArgumentError)


654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
# File 'lib/little_ghost/agent.rb', line 654

def interrupt_response(
  message,
  cancellation_token: Support::CancellationToken.new,
  deadline: nil,
  target_operation_id: nil,
  interruption_id: nil,
  batch_key: nil,
  metadata: {}
)
  message = Message.new(role: :user, content: message) if message.is_a?(String)
  raise ArgumentError, "interrupt message must be a String or LittleGhost::Message" unless message.is_a?(Message)
  safe_content = message.content.all? do |content|
    content.is_a?(Content::Text) ||
      content.is_a?(Content::Image) ||
      content.is_a?(Content::Document)
  end
  unless safe_content
    raise ArgumentError, "interrupt message content must contain only text, images, or documents"
  end

  interruptions = @interruptions_mutex.synchronize do
    active = if target_operation_id
      @active_interruptions.select { |candidate| candidate.target_operation_id == target_operation_id }
    else
      @active_interruptions
    end
    if active.empty?
      raise AgentInterruptError, "Agent is not currently running"
    end
    if active.length > 1
      raise AgentInterruptError, "Agent has multiple active invocations; the interruption target is ambiguous"
    end

    active.first
  end
  options = {batch_key:, metadata:}
  options[:id] = interruption_id unless interruption_id.nil?
  ticket = interruptions.enqueue(message, **options)
  instrument(
    :agent_interrupt_queued,
    parent_operation_id: interruptions.operation_id,
    interruption_id: ticket.id,
    event_kind: :interrupt,
    diagnostic: {input: diagnostic_message(message)}
  )
  begin
    response = ticket.value(cancellation_token:, deadline:)
    interruptions.release(ticket)
    response
  rescue => error
    interruptions.release(ticket, withdraw: true)
    instrument(
      :agent_interrupt_failed,
      parent_operation_id: interruptions.operation_id,
      interruption_id: ticket.id,
      event_kind: :interrupt,
      error_type: error.class.name,
      diagnostic: {exception: diagnostic_exception(error)}
    )
    raise
  end
end

#prompt_localsObject

Materializes and freezes the prompt locals declared on the agent class.



868
869
870
871
872
873
874
875
876
877
# File 'lib/little_ghost/agent.rb', line 868

def prompt_locals
  self.class.prompt_local_resolvers.to_h do |name, resolver|
    value = if resolver.respond_to?(:call)
      resolver.parameters.empty? ? instance_exec(&resolver) : resolver.call(self)
    else
      resolver
    end
    [name, value]
  end.freeze
end

#stream(input = nil, history: nil, context: nil, cancellation_token: Support::CancellationToken.new, deadline: nil, settings: nil, template_locals: nil, template_paths: nil, parent_operation_id: nil, checkpoint: nil, conversation_id: nil, interruption_metadata: nil, interruption_ids: [], interrupt_ready: nil) ⇒ Object

Streams one invocation as StreamEvent objects.

Agents built inside a run accept history, JSON-like context, cancellation, deadlines, settings, and trusted invocation template paths. An agent instance may be streamed only according to the lifecycle managed by its owning run. Every template path must be an application-created TrustedPath; the wrapper records a trust decision and must never contain unchecked request or model input.

Raises:

  • (ArgumentError)


725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
# File 'lib/little_ghost/agent.rb', line 725

def stream(
  input = nil,
  history: nil,
  context: nil,
  cancellation_token: Support::CancellationToken.new,
  deadline: nil,
  settings: nil,
  template_locals: nil,
  template_paths: nil,
  parent_operation_id: nil,
  checkpoint: nil,
  conversation_id: nil,
  interruption_metadata: nil,
  interruption_ids: [],
  interrupt_ready: nil
)
  if @standalone
    raise ArgumentError, "input is required" if input.nil?

    return build_run(entrypoint_payload(input, {
      history:,
      context:,
      settings:,
      template_paths:,
      deadline_at: deadline
    }.compact)).each
  end

  raise ArgumentError, "input is required" if input.nil?

  history ||= []
  context ||= {}
  settings ||= {}
  template_locals ||= {}
  template_paths ||= []
  invocation_paths = Array(template_paths).map do |path|
    unless path.is_a?(LittleGhost::TrustedPath)
      raise ArgumentError, "invocation template paths must be LittleGhost::TrustedPath values"
    end
    path
  end
  settings = @model_settings.merge(settings)
  Enumerator.new do |events|
    interruptions = AgentInterruptions.new
    run_context = RunContext.new(
      state: context,
      cancellation_token: cancellation_token,
      deadline: deadline,
      metadata: {agent_id: self.class.agent_id},
      checkpoint:,
      conversation_id:,
      interruption_metadata:,
      interruption_ids:
    )
    begin
      with_invocation(run_context) do
        execute(
          input,
          history: history,
          context: run_context,
          settings: settings,
          template_locals: template_locals,
          template_paths: invocation_paths,
          events: events,
          parent_operation_id:,
          interruptions:,
          interrupt_ready:
        )
      end
    rescue => error
      interruptions.close(error)
      raise
    ensure
      interruptions.close(AgentInterruptError.new("Agent finished before the interruption was delivered"))
      unregister_interruptions(interruptions)
    end
  end
end

#stream_ask(message, **options) ⇒ Object

Streams message through the standalone or run-scoped agent. Standalone options become Invocation fields; run-scoped options are forwarded to #stream.



807
808
809
810
811
812
813
814
815
816
817
818
# File 'lib/little_ghost/agent.rb', line 807

def stream_ask(message, **options)
  if @standalone
    options[:deadline_at] = options.delete(:deadline) if options.key?(:deadline)
    stream = nil
    return Enumerator.new do |events|
      stream ||= build_run(entrypoint_payload(message, options)).each
      stream.each { |event| events << event }
    end
  end

  stream(message, **options)
end

#toolsObject

The materialized tools available during this agent run.



880
# File 'lib/little_ghost/agent.rb', line 880

def tools = tool_registry