Class: LittleGhost::Agent

Inherits:
Assembly 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

Defines one reusable model-driven behavior with prompts, tools, and limits. Each Agent subclass describes one application role with an inheritable Ruby DSL. It can answer, stream, call tools, and delegate work.

Start with one role and add capabilities as its work grows:

class CustomerSupportAgent < LittleGhost::Agent
description "Handles support requests"
model "openrouter:openai/gpt-5.6-luna"
system_prompt "Answer customer questions clearly."
end

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

An Agent is the smallest Assembly: it owns one model loop while inheriting the same ask and stream_ask entrypoints as coordinated assemblies. Add tools for application operations and subagents for model-directed delegation.

Call a named Agent with ask when you need the final Run, or the streaming entrypoint when you want events as the answer arrives.

Most applications call a named Agent class. LittleGhost automatically reuses the active Configuration's shared Runtime while building a fresh top-level Run for every call. Passing runtime: is an advanced option for an explicitly isolated setup.

Agent declarations are inherited. Define a short prompt inline, or place a growing prompt in app/prompts/customer_support/system_prompt.erb for CustomerSupportAgent. The Prompts as Views guide explains conventional lookup, values prepared by the Agent, locals, and partials. Optional features such as skills, context management, loop detection, and delegation stay inactive until their DSL is used.

Models may return text or locally validated structured data. LittleGhost hides unexpected Tool exception messages from the model. See Run for outcomes, cancellation, and cleanup, and Assembly for the advanced run-scoped form.

Defined Under Namespace

Modules: ContextManagement, Delegation, Skills, ToolLoop Classes: ExecutedTool

Constant Summary collapse

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

Constants included from ToolLoop

ToolLoop::TRACKED_INVOCATION_LIMIT

Constants inherited from Assembly

LittleGhost::Assembly::MAX_STEP_EVENTS, LittleGhost::Assembly::MAX_STEP_EVENT_BYTES, LittleGhost::Assembly::MAX_STEP_OUTPUT_BYTES

Instance Attribute Summary collapse

Attributes inherited from Assembly

#agent_stream_path

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

Methods inherited from Assembly

#as_tool, #as_tool_with_factory, ask, #ask, assembly_id, assembly_kind, #bind_agent_stream_path, #bind_assembly_definition, #build_run, #call, definition, description, #prompt_locals, #start_execution, stream_ask, #stream_ask, to_builder, validate_step_policy!

Constructor Details

#initialize(model: nil, runtime: nil, tools: [], template_resolver: nil, template_paths: [], run: nil, executor: nil, 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. :call-seq:

new(runtime: nil) -> Agent
new(model:, runtime:, tools:, run:, ...) -> Agent

The first form is the application-facing entrypoint. It may be reused for independent concurrent calls and creates a fresh Run for each one. The second form is run-scoped; Runtime builders supply its dependencies and it must not outlive its owning Run. Calls on a run-scoped instance must be sequential. An overlapping call raises AgentBusyError when its stream begins.



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
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
# File 'lib/little_ghost/agent.rb', line 532

def initialize(
  model: nil,
  runtime: nil,
  tools: [],
  template_resolver: nil,
  template_paths: [],
  run: nil,
  executor: nil,
  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
)
  standalone = model.nil? && run.nil?
  super(run:, runtime:, workspace:, sandbox:, standalone:)
  if standalone
    @framework_prompts = FrameworkPrompts.new(paths: template_paths)
    @framework_prompt_invocation_paths = [].freeze
    @owns_resources = true
    @closed = false
    @close_mutex = Mutex.new
    @interjections_mutex = Mutex.new
    @active_interjections = []
    return
  end

  @model = model
  @runtime = runtime || run&.runtime
  @framework_prompts = if @runtime
    FrameworkPrompts.for_runtime(@runtime)
  else
    FrameworkPrompts.new(paths: template_paths)
  end
  @framework_prompt_invocation_paths = [].freeze
  @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
  initialize_code_mode
  @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 || Support::Executor.new(runner: task_runner)
  @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
  @interjections_mutex = Mutex.new
  @active_interjections = []
  @assembly_transitions_mutex = Mutex.new
  @assembly_transitions = {}
  @assembly_tool_batch_sizes = {}
  @assembly_transition = nil
  @_little_ghost_invocation_mutex = Mutex.new
  @_little_ghost_invocation_active = false
  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
  @artifact_lifecycle = @runtime&.then do |resolved_runtime|
    resolved_runtime.runtime_hooks.find { |hook| hook.is_a?(Runtime::Hooks::Artifacts) }
  end
  apply_cancellation_decision!(run_callbacks(:after_initialize, self))
  @_little_ghost_prompt_assign_baseline = prompt_application_state.freeze
rescue
  @tool_registry&.close
  @code_mode_runtime&.close
  raise
end

Instance Attribute Details

#agent_pathObject (readonly)

This Agent's location in the bounded subagent tree.



514
515
516
# File 'lib/little_ghost/agent.rb', line 514

def agent_path
  @agent_path
end

#assembly_transitionObject (readonly)

:nodoc:



657
658
659
# File 'lib/little_ghost/agent.rb', line 657

def assembly_transition
  @assembly_transition
end

#delegation_activityObject (readonly)

Shared delegation tracker, when subagents are enabled.



512
513
514
# File 'lib/little_ghost/agent.rb', line 512

def delegation_activity
  @delegation_activity
end

#max_tool_callsObject (readonly)

Maximum Tool calls allowed during one invocation.



520
521
522
# File 'lib/little_ghost/agent.rb', line 520

def max_tool_calls
  @max_tool_calls
end

#modelObject (readonly)

The resolved model used by this run-scoped Agent.



506
507
508
# File 'lib/little_ghost/agent.rb', line 506

def model
  @model
end

#runObject (readonly)

The owning Run, or nil for a standalone entrypoint.



510
511
512
# File 'lib/little_ghost/agent.rb', line 510

def run
  @run
end

#runtimeObject (readonly)

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



623
624
625
# File 'lib/little_ghost/agent.rb', line 623

def runtime
  @runtime
end

#sandboxObject (readonly)

Run-scoped sandbox used for filesystem and process operations.



518
519
520
# File 'lib/little_ghost/agent.rb', line 518

def sandbox
  @sandbox
end

#tool_registryObject (readonly)

Tools created and bound for this Agent's owning Run.



508
509
510
# File 'lib/little_ghost/agent.rb', line 508

def tool_registry
  @tool_registry
end

#workspaceObject (readonly)

Run-scoped workspace available to Tools and extensions.



516
517
518
# File 'lib/little_ghost/agent.rb', line 516

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.



96
97
98
99
100
# File 'lib/little_ghost/agent.rb', line 96

def agent_id(*values)
  return assembly_id if values.empty?

  assembly_id(values.fetch(0))
end

.callbacksObject

:nodoc:



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

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.



214
215
216
217
218
# File 'lib/little_ghost/agent.rb', line 214

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

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

.code_mode(engine: nil, except: nil, **options) ⇒ Object

Enables code mode for this agent. except names the application Tools that remain model-facing; every other application Tool moves into the engine catalog and is called through the parent-process Broker. Framework-owned subagent controls remain model-facing automatically.

Raises:

  • (ArgumentError)


275
276
277
278
279
280
281
# File 'lib/little_ghost/agent.rb', line 275

def code_mode(engine: nil, except: nil, **options)
  unknown = options.keys - %i[sandbox limits]
  raise ArgumentError, "unknown keyword: #{unknown.first.inspect}" unless unknown.empty?

  declaration = options.merge(engine:, except:).compact
  self.code_mode_configuration_value = declaration.freeze
end

.code_mode_configurationObject

:nodoc:



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

def code_mode_configuration = code_mode_configuration_value # :nodoc:

.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.



149
150
151
152
153
# File 'lib/little_ghost/agent.rb', line 149

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.



103
104
105
106
107
# File 'lib/little_ghost/agent.rb', line 103

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")


124
125
126
127
128
129
130
131
132
133
134
# File 'lib/little_ghost/agent.rb', line 124

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:



136
137
138
139
# File 'lib/little_ghost/agent.rb', line 136

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

.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.

During execution, a missing or invalid result receives one repair attempt before LittleGhost::StructuredResultError is raised inside the owning Run. A top-level ask records it on a failed Run. Invalid schemas and strategies raise LittleGhost::ConfigurationError before execution begins.

Raises:

  • (ArgumentError)


169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/little_ghost/agent.rb', line 169

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

.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.



240
241
242
243
244
245
246
247
248
249
250
251
# File 'lib/little_ghost/agent.rb', line 240

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.



225
226
227
228
229
# File 'lib/little_ghost/agent.rb', line 225

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

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

.tool_declarationsObject

:nodoc:



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

def tool_declarations = tool_declarations_value # :nodoc:

.tools(*values) ⇒ Object

Adds tool or provider classes to the agent.

Every declaration must be a class. Pass Tool classes directly, or pass provider classes that supply tools dynamically through tools(binding). Multiple declarations are cumulative.



258
259
260
261
262
263
264
265
266
267
# File 'lib/little_ghost/agent.rb', line 258

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

#closeObject

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



871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
# File 'lib/little_ghost/agent.rb', line 871

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

    @closed = true
    [
      [@code_mode_runtime, tool_registry, (@sandbox if @owns_resources), (@workspace if @owns_resources)],
      @interjections_mutex.synchronize { @active_interjections.dup }
    ]
  end
  first_error = nil
  interjections.each do |active|
    active.close(AgentInterjectionError.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

#code_mode_runtimeObject

:nodoc:



639
640
641
# File 'lib/little_ghost/agent.rb', line 639

def code_mode_runtime # :nodoc:
  @code_mode_runtime || raise(ConfigurationError, "code mode is not enabled for this agent")
end

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

:nodoc:



627
628
629
630
631
632
633
634
635
636
637
# File 'lib/little_ghost/agent.rb', line 627

def dispatch_tools(tool_uses, context:, events:, parent_operation_id:, parent_trace_context: nil) # :nodoc:
  counted = tool_uses.count { |tool_use| !code_mode_control_tool?(tool_use) }
  context.record_tool_calls!(counted, maximum: @max_tool_calls) if counted.positive?
  execute_tools(
    tool_uses,
    context,
    events,
    parent_operation_id:,
    parent_trace_context:
  )
end

#entrypoint_nameObject

:nodoc:



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

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

#framework_prompt_scopeObject

:nodoc:



2293
2294
2295
2296
2297
2298
2299
# File 'lib/little_ghost/agent.rb', line 2293

def framework_prompt_scope # :nodoc:
  {
    framework_prompts: @framework_prompts,
    invocation_paths: @framework_prompt_invocation_paths,
    agent_path: self.class.logical_path
  }.freeze
end

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

Adds an interjection and returns the model's immediate result details.

target_operation_id may identify the active model operation. Messages may contain only text, image, or document content. The returned result value exposes text, tool_calls?, interjection_ids, and batch_key; tool calls may continue after this result. Depend on these methods rather than the result's concrete class.

Raises:

  • (ArgumentError)


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
716
717
718
719
720
721
722
723
724
725
726
727
# File 'lib/little_ghost/agent.rb', line 666

def interject(
  message,
  cancellation_token: Support::CancellationToken.new,
  deadline: nil,
  target_operation_id: nil,
  interjection_id: nil,
  batch_key: nil,
  metadata: {}
)
  message = Message.new(role: :user, content: message) if message.is_a?(String)
  raise ArgumentError, "interject 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, "interject message content must contain only text, images, or documents"
  end

  interjections = @interjections_mutex.synchronize do
    active = if target_operation_id
      @active_interjections.select { |candidate| candidate.target_operation_id == target_operation_id }
    else
      @active_interjections
    end
    if active.empty?
      raise AgentInterjectionError, "Agent is not currently running"
    end
    if active.length > 1
      raise AgentInterjectionError, "Agent has multiple active invocations; the interjection target is ambiguous"
    end

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

#render_framework_prompt(key, **locals) ⇒ Object

:nodoc:



2284
2285
2286
2287
2288
2289
2290
2291
# File 'lib/little_ghost/agent.rb', line 2284

def render_framework_prompt(key, **locals) # :nodoc:
  @framework_prompts.render(
    key,
    locals:,
    invocation_paths: @framework_prompt_invocation_paths,
    agent_path: self.class.logical_path
  )
end

#request_assembly_transition(value, context:) ⇒ Object

:nodoc:



643
644
645
646
647
648
649
650
651
652
653
654
655
# File 'lib/little_ghost/agent.rb', line 643

def request_assembly_transition(value, context:) # :nodoc:
  @assembly_transitions_mutex.synchronize do
    unless @assembly_tool_batch_sizes[context] == 1
      raise ToolError, render_framework_prompt("assembly/feedback/transition_only")
    end
    if @assembly_transitions.key?(context)
      raise ProtocolError, "Multiple assembly transitions were requested in one agent turn"
    end

    @assembly_transitions[context] = value
  end
  value
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, interjection_metadata: nil, interjection_ids: [], interject_ready: nil) ⇒ Object

Streams one invocation as StreamEvent objects.

Agents built inside a run accept history, JSON-like context, cancellation, deadlines, settings, and invocation-specific prompt roots. An Agent instance may be streamed only by its owning Run. Every template path must be an application-created TrustedPath, never a value selected by a request or model. A run-scoped Agent accepts one active invocation; enumerating an overlapping stream raises AgentBusyError. The instance may be invoked again after the first stream completes or fails.

Raises:

  • (ArgumentError)


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
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
# File 'lib/little_ghost/agent.rb', line 738

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,
  interjection_metadata: nil,
  interjection_ids: [],
  interject_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,
      cancellation_token:
    }.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|
    invocation_acquired = false
    interjections = AgentInterjections.new
    begin
      begin_invocation!
      invocation_acquired = true
      run_context = RunContext.new(
        state: context,
        cancellation_token: cancellation_token,
        deadline: deadline,
        metadata: {agent_id: self.class.agent_id},
        checkpoint:,
        conversation_id:,
        interjection_metadata:,
        interjection_ids:
      )
      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:,
          interjections:,
          interject_ready:
        )
      end
    rescue => error
      interjections.close(error)
      raise
    ensure
      cleanup_error = nil
      begin
        @code_mode_runtime&.close(context: run_context) if run_context
      rescue => error
        cleanup_error = error
      end
      begin
        interjections.close(AgentInterjectionError.new("Agent finished before the interjection was delivered"))
      rescue => error
        cleanup_error ||= error
      ensure
        begin
          unregister_interjections(interjections)
        ensure
          end_invocation! if invocation_acquired
        end
      end
      raise cleanup_error if cleanup_error
    end
  end
end

#system_promptObject

:call-seq:

system_prompt() -> Object

Prepares application values for a file-backed system prompt. Override this method, assign instance variables, and read them from the ERB view:

def system_prompt
@company_name = "Northstar"
end

In app/prompts/customer_support/system_prompt.erb:

You help customers of <%= @company_name %>.

LittleGhost calls the action once before rendering the top-level prompt view. Partials share its prepared values without calling it again. Its return value is ignored. If it raises, the invocation fails with that exception. An inline prompt declared on the Agent class takes precedence and does not call this method.

Each render starts with the application instance variables captured after the Agent's after_initialize callbacks finish. LittleGhost copies the prepared values into the view, then restores the Agent's live instance variables, even when this method raises. Objects are not duplicated, so treat mutable values as read-only or assign a copy. Subclasses may call super before preparing additional values.



863
864
# File 'lib/little_ghost/agent.rb', line 863

def system_prompt
end

#toolsObject

The Tool registry available during this Agent run.



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

def tools = tool_registry