little_ghost 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +68 -84
  3. data/docs/guides/assemblies.md +286 -0
  4. data/docs/guides/core_concepts.md +126 -231
  5. data/docs/guides/getting_started.md +114 -87
  6. data/docs/guides/production.md +187 -0
  7. data/docs/guides/prompt_views.md +132 -0
  8. data/lib/little_ghost/ag_ui/adapter.rb +3 -3
  9. data/lib/little_ghost/agent/delegation.rb +1 -1
  10. data/lib/little_ghost/agent.rb +167 -172
  11. data/lib/little_ghost/{agent_interruptions.rb → agent_interjections.rb} +12 -12
  12. data/lib/little_ghost/assembly.rb +55 -21
  13. data/lib/little_ghost/assembly_builder.rb +40 -2
  14. data/lib/little_ghost/assembly_execution.rb +87 -4
  15. data/lib/little_ghost/configuration.rb +263 -39
  16. data/lib/little_ghost/content.rb +5 -5
  17. data/lib/little_ghost/data_map.rb +209 -0
  18. data/lib/little_ghost/errors.rb +2 -2
  19. data/lib/little_ghost/execution.rb +32 -32
  20. data/lib/little_ghost/graph.rb +22 -3
  21. data/lib/little_ghost/message.rb +4 -4
  22. data/lib/little_ghost/model_resolver.rb +2 -2
  23. data/lib/little_ghost/prompt_resolver.rb +2 -0
  24. data/lib/little_ghost/run.rb +87 -49
  25. data/lib/little_ghost/run_context.rb +33 -20
  26. data/lib/little_ghost/runtime/hook.rb +3 -3
  27. data/lib/little_ghost/runtime.rb +71 -31
  28. data/lib/little_ghost/session.rb +12 -23
  29. data/lib/little_ghost/session_store.rb +9 -5
  30. data/lib/little_ghost/session_stores/agent_core_memory.rb +64 -56
  31. data/lib/little_ghost/session_stores/filesystem.rb +261 -0
  32. data/lib/little_ghost/session_stores/memory.rb +7 -0
  33. data/lib/little_ghost/subagents/manager.rb +42 -42
  34. data/lib/little_ghost/swarm.rb +13 -5
  35. data/lib/little_ghost/tool.rb +56 -14
  36. data/lib/little_ghost/tools/write_todos.rb +6 -1
  37. data/lib/little_ghost/tracing/open_telemetry.rb +1 -1
  38. data/lib/little_ghost/version.rb +1 -1
  39. data/lib/little_ghost/workflow.rb +30 -21
  40. data/lib/little_ghost.rb +29 -25
  41. metadata +7 -2
@@ -10,46 +10,49 @@ module LittleGhost
10
10
  # Each Agent subclass describes one application role with an inheritable Ruby
11
11
  # DSL. It can answer, stream, call tools, and delegate work.
12
12
  #
13
- # A customer support agent can look up an account itself and give longer
14
- # investigations to a research specialist:
15
- #
16
- # class ResearchAgent < LittleGhost::Agent
17
- # description "Researches transfer failures"
18
- # model "customer_support.research"
19
- # tools LedgerSearchTool
20
- # end
13
+ # Start with one role and add capabilities as its work grows:
21
14
  #
22
15
  # class CustomerSupportAgent < LittleGhost::Agent
23
16
  # description "Handles support requests"
24
- # model :customer_support
25
- # limits max_turns: 40
26
- # tools AccountLookupTool
27
- # subagent ResearchAgent, kind: "research"
17
+ # model "openrouter:openai/gpt-5.6-luna"
18
+ # system_prompt "Answer customer questions clearly."
28
19
  # end
29
20
  #
30
21
  # run = CustomerSupportAgent.ask("Why is transfer 481 pending?")
31
22
  # run.completed? # => true
32
- # run.response # => "Transfer 481 is waiting for the receiving bank."
23
+ # run.response
24
+ # # One possible response: Transfer 481 is waiting for the receiving bank.
25
+ #
26
+ # An Agent is the smallest Assembly: it owns one model loop while inheriting
27
+ # the same +ask+ and +stream_ask+ entrypoints as coordinated assemblies.
28
+ # Add tools for application operations and subagents for model-directed
29
+ # delegation.
30
+ #
31
+ # Call a named Agent with
32
+ # ask[rdoc-ref:LittleGhost::Assembly.ask] when you need the final Run, or
33
+ # the streaming entrypoint[rdoc-ref:LittleGhost::Assembly.stream_ask] when you
34
+ # want events as the answer arrives.
33
35
  #
34
- # Class declarations are inherited. Prompts resolve by the agent's logical
35
- # path unless +system_prompt+ or +system_template+ supplies one explicitly;
36
- # tools and prompt locals may also be selected dynamically for each run.
37
- # Capabilities such as skills, context management, loop detection, and
38
- # delegation remain inactive until their DSL methods are called.
36
+ # Most applications call a named Agent class. LittleGhost automatically
37
+ # reuses the active Configuration's shared Runtime while building a fresh
38
+ # top-level Run for every call. Passing +runtime:+ is an advanced option for
39
+ # an explicitly isolated setup.
39
40
  #
40
- # The class-level +ask+ and +stream_ask+ helpers create fresh standalone
41
- # entrypoints. Create an instance explicitly to reuse one Runtime across
42
- # calls. Runtimes build bound instances internally; their +call+ method
43
- # returns a RunResult and their +stream+ method follows the owning run's
44
- # single-execution lifecycle. Closing an agent closes owned tools and any
45
- # standalone workspace and sandbox.
46
- # LittleGhost::Agent.ask uses <tt>You are a helpful agent.</tt> as its system
47
- # prompt. Subclasses continue to use their inline or conventional prompts.
41
+ # Agent declarations are inherited. Define a short prompt inline, or place a
42
+ # growing prompt in <tt>app/prompts/customer_support/system.erb</tt> for
43
+ # +CustomerSupportAgent+. The {Prompts as Views guide}[rdoc-ref:docs/guides/prompt_views.md]
44
+ # explains conventional lookup, locals, and partials. Optional features such
45
+ # as skills, context management, loop detection, and delegation stay inactive
46
+ # until their DSL is used.
48
47
  #
49
- # Models can return ordinary text or a locally validated structured result.
50
- # Tool failures are sanitized before returning to the model, diagnostic
51
- # capture can be disabled for sensitive agents, and cancellation, deadlines,
52
- # and cleanup failures remain framework control flow.
48
+ # Calling LittleGhost::Agent itself uses <tt>You are a helpful agent.</tt> as
49
+ # the system prompt. Subclasses use the inline or conventional prompt they
50
+ # declare.
51
+ #
52
+ # Models may return text or locally validated structured data. LittleGhost
53
+ # hides unexpected Tool exception messages from the model. See
54
+ # Run[rdoc-ref:LittleGhost::Run] for outcomes, cancellation, and cleanup, and
55
+ # Assembly[rdoc-ref:LittleGhost::Assembly] for the advanced run-scoped form.
53
56
  class Agent < Assembly
54
57
  DEFAULT_SYSTEM_PROMPT = "You are a helpful agent." # :nodoc:
55
58
  DEFAULT_MAX_TOOL_RESULT_TOKENS = 10_000 # :nodoc:
@@ -161,9 +164,10 @@ module LittleGhost
161
164
  # strategy selection prefers provider-native structured output and falls
162
165
  # back to a terminal tool when supported.
163
166
  #
164
- # A missing or invalid result receives one repair attempt before
165
- # LittleGhost::StructuredResultError is raised. Invalid schemas and
166
- # strategies raise LittleGhost::ConfigurationError immediately.
167
+ # During execution, a missing or invalid result receives one repair attempt
168
+ # before LittleGhost::StructuredResultError is raised inside the owning Run.
169
+ # A top-level +ask+ records it on a failed Run. Invalid schemas and strategies
170
+ # raise LittleGhost::ConfigurationError before execution begins.
167
171
  def result_schema(schema = nil, name: nil, description: nil, strategy: :auto, **schema_keywords)
168
172
  return result_schema_value if schema.nil? && schema_keywords.empty? && name.nil? && description.nil? && strategy == :auto
169
173
 
@@ -466,16 +470,32 @@ module LittleGhost
466
470
  end
467
471
  end
468
472
 
469
- # Run-scoped model, tools, lifecycle, delegation, and execution resources
470
- # available to agent extensions.
471
- attr_reader :model, :tool_registry, :run, :delegation_activity, :agent_path, :workspace, :sandbox,
472
- :max_tool_calls
473
+ # The resolved model used by this run-scoped Agent.
474
+ attr_reader :model
475
+ # Tools created and bound for this Agent's owning Run.
476
+ attr_reader :tool_registry
477
+ # The owning Run, or +nil+ for a standalone entrypoint.
478
+ attr_reader :run
479
+ # Shared delegation tracker, when subagents are enabled.
480
+ attr_reader :delegation_activity
481
+ # This Agent's location in the bounded subagent tree.
482
+ attr_reader :agent_path
483
+ # Run-scoped workspace available to Tools and extensions.
484
+ attr_reader :workspace
485
+ # Run-scoped sandbox used for filesystem and process operations.
486
+ attr_reader :sandbox
487
+ # Maximum Tool calls allowed during one invocation.
488
+ attr_reader :max_tool_calls
473
489
 
474
490
  # Creates either a standalone entrypoint or a run-scoped agent.
491
+ # :call-seq:
492
+ # new(runtime: nil) -> Agent
493
+ # new(model:, runtime:, tools:, run:, ...) -> Agent
475
494
  #
476
- # Calling <tt>new</tt> without +model+ and +run+ creates the standalone form
477
- # used by +ask+ and +stream_ask+. Runtime builders supply the remaining
478
- # dependencies and apply class-level limits and declarations.
495
+ # The first form is the application-facing entrypoint. It may be reused for
496
+ # independent concurrent calls and creates a fresh Run for each one. The
497
+ # second form is run-scoped; Runtime builders supply its dependencies and it
498
+ # must not outlive or be shared outside its owning Run.
479
499
  def initialize(
480
500
  model: nil,
481
501
  runtime: nil,
@@ -499,8 +519,8 @@ module LittleGhost
499
519
  @owns_resources = true
500
520
  @closed = false
501
521
  @close_mutex = Mutex.new
502
- @interruptions_mutex = Mutex.new
503
- @active_interruptions = []
522
+ @interjections_mutex = Mutex.new
523
+ @active_interjections = []
504
524
  return
505
525
  end
506
526
 
@@ -535,8 +555,8 @@ module LittleGhost
535
555
  @closed = false
536
556
  @close_mutex = Mutex.new
537
557
  @exclusive_tools_mutex = Mutex.new
538
- @interruptions_mutex = Mutex.new
539
- @active_interruptions = []
558
+ @interjections_mutex = Mutex.new
559
+ @active_interjections = []
540
560
  @assembly_transitions_mutex = Mutex.new
541
561
  @assembly_transitions = {}
542
562
  @assembly_tool_batch_sizes = {}
@@ -575,94 +595,69 @@ module LittleGhost
575
595
 
576
596
  attr_reader :assembly_transition # :nodoc:
577
597
 
578
- # Adds +message+ to one active invocation and waits for its ordinary text reply.
579
- #
580
- # The response may continue into tool calls; delivery does not stop the
581
- # original invocation. Raises LittleGhost::AgentInterruptError when there is
582
- # no unambiguous active target.
583
- def interrupt(
584
- message,
585
- cancellation_token: Support::CancellationToken.new,
586
- deadline: nil,
587
- target_operation_id: nil,
588
- interruption_id: nil,
589
- batch_key: nil,
590
- metadata: {}
591
- )
592
- interrupt_response(
593
- message,
594
- cancellation_token:,
595
- deadline:,
596
- target_operation_id:,
597
- interruption_id:,
598
- batch_key:,
599
- metadata:
600
- ).text
601
- end
602
-
603
- # Adds an interruption and returns the model's immediate response details.
598
+ # Adds an interjection and returns the model's immediate result details.
604
599
  #
605
600
  # Use +target_operation_id+ when an agent has multiple active invocations.
606
601
  # Messages may contain only text, image, or document content. The returned
607
- # response value exposes +text+, +tool_calls?+, +interruption_ids+, and
608
- # +batch_key+; tool calls may continue after this response. Depend on these
609
- # methods rather than the response's concrete class.
610
- def interrupt_response(
602
+ # result value exposes +text+, +tool_calls?+, +interjection_ids+, and
603
+ # +batch_key+; tool calls may continue after this result. Depend on these
604
+ # methods rather than the result's concrete class.
605
+ def interject(
611
606
  message,
612
607
  cancellation_token: Support::CancellationToken.new,
613
608
  deadline: nil,
614
609
  target_operation_id: nil,
615
- interruption_id: nil,
610
+ interjection_id: nil,
616
611
  batch_key: nil,
617
612
  metadata: {}
618
613
  )
619
614
  message = Message.new(role: :user, content: message) if message.is_a?(String)
620
- raise ArgumentError, "interrupt message must be a String or LittleGhost::Message" unless message.is_a?(Message)
615
+ raise ArgumentError, "interject message must be a String or LittleGhost::Message" unless message.is_a?(Message)
621
616
  safe_content = message.content.all? do |content|
622
617
  content.is_a?(Content::Text) ||
623
618
  content.is_a?(Content::Image) ||
624
619
  content.is_a?(Content::Document)
625
620
  end
626
621
  unless safe_content
627
- raise ArgumentError, "interrupt message content must contain only text, images, or documents"
622
+ raise ArgumentError, "interject message content must contain only text, images, or documents"
628
623
  end
629
624
 
630
- interruptions = @interruptions_mutex.synchronize do
625
+ interjections = @interjections_mutex.synchronize do
631
626
  active = if target_operation_id
632
- @active_interruptions.select { |candidate| candidate.target_operation_id == target_operation_id }
627
+ @active_interjections.select { |candidate| candidate.target_operation_id == target_operation_id }
633
628
  else
634
- @active_interruptions
629
+ @active_interjections
635
630
  end
636
631
  if active.empty?
637
- raise AgentInterruptError, "Agent is not currently running"
632
+ raise AgentInterjectionError, "Agent is not currently running"
638
633
  end
639
634
  if active.length > 1
640
- raise AgentInterruptError, "Agent has multiple active invocations; the interruption target is ambiguous"
635
+ raise AgentInterjectionError, "Agent has multiple active invocations; the interjection target is ambiguous"
641
636
  end
642
637
 
643
638
  active.first
644
639
  end
645
640
  options = {batch_key:, metadata:}
646
- options[:id] = interruption_id unless interruption_id.nil?
647
- ticket = interruptions.enqueue(message, **options)
641
+ options[:id] = interjection_id unless interjection_id.nil?
642
+ ticket = interjections.enqueue(message, **options)
648
643
  instrument(
649
- :agent_interrupt_queued,
650
- parent_operation_id: interruptions.operation_id,
651
- interruption_id: ticket.id,
652
- event_kind: :interrupt,
644
+ :agent_interjection_queued,
645
+ parent_operation_id: interjections.operation_id,
646
+ interjection_id: ticket.id,
647
+ event_kind: :interjection,
653
648
  diagnostic: {input: diagnostic_message(message)}
654
649
  )
655
650
  begin
656
651
  response = ticket.value(cancellation_token:, deadline:)
657
- interruptions.release(ticket)
652
+ interjections.release(ticket)
658
653
  response
659
654
  rescue => error
660
- interruptions.release(ticket, withdraw: true)
655
+ interjections.release(ticket, withdraw: true)
661
656
  instrument(
662
- :agent_interrupt_failed,
663
- parent_operation_id: interruptions.operation_id,
664
- interruption_id: ticket.id,
665
- event_kind: :interrupt,
657
+ :agent_interjection_failed,
658
+ parent_operation_id: interjections.operation_id,
659
+ interjection_id: ticket.id,
660
+ event_kind: :interjection,
666
661
  error_type: error.class.name,
667
662
  diagnostic: {exception: diagnostic_exception(error)}
668
663
  )
@@ -690,9 +685,9 @@ module LittleGhost
690
685
  parent_operation_id: nil,
691
686
  checkpoint: nil,
692
687
  conversation_id: nil,
693
- interruption_metadata: nil,
694
- interruption_ids: [],
695
- interrupt_ready: nil
688
+ interjection_metadata: nil,
689
+ interjection_ids: [],
690
+ interject_ready: nil
696
691
  )
697
692
  if standalone?
698
693
  raise ArgumentError, "input is required" if input.nil?
@@ -722,7 +717,7 @@ module LittleGhost
722
717
  end
723
718
  settings = @model_settings.merge(settings)
724
719
  Enumerator.new do |events|
725
- interruptions = AgentInterruptions.new
720
+ interjections = AgentInterjections.new
726
721
  run_context = RunContext.new(
727
722
  state: context,
728
723
  cancellation_token: cancellation_token,
@@ -730,8 +725,8 @@ module LittleGhost
730
725
  metadata: {agent_id: self.class.agent_id},
731
726
  checkpoint:,
732
727
  conversation_id:,
733
- interruption_metadata:,
734
- interruption_ids:
728
+ interjection_metadata:,
729
+ interjection_ids:
735
730
  )
736
731
  begin
737
732
  with_invocation(run_context) do
@@ -744,16 +739,16 @@ module LittleGhost
744
739
  template_paths: invocation_paths,
745
740
  events: events,
746
741
  parent_operation_id:,
747
- interruptions:,
748
- interrupt_ready:
742
+ interjections:,
743
+ interject_ready:
749
744
  )
750
745
  end
751
746
  rescue => error
752
- interruptions.close(error)
747
+ interjections.close(error)
753
748
  raise
754
749
  ensure
755
- interruptions.close(AgentInterruptError.new("Agent finished before the interruption was delivered"))
756
- unregister_interruptions(interruptions)
750
+ interjections.close(AgentInterjectionError.new("Agent finished before the interjection was delivered"))
751
+ unregister_interjections(interjections)
757
752
  end
758
753
  end
759
754
  end
@@ -773,21 +768,21 @@ module LittleGhost
773
768
  # The materialized tools available during this agent run.
774
769
  def tools = tool_registry
775
770
 
776
- # Closes owned tools, interruptions, sandbox, and workspace resources.
771
+ # Closes owned tools, interjections, sandbox, and workspace resources.
777
772
  # The operation is idempotent and re-raises the first cleanup failure.
778
773
  def close
779
- resources, interruptions = @close_mutex.synchronize do
774
+ resources, interjections = @close_mutex.synchronize do
780
775
  return if @closed
781
776
 
782
777
  @closed = true
783
778
  [
784
779
  [tool_registry, (@sandbox if @owns_resources), (@workspace if @owns_resources)],
785
- @interruptions_mutex.synchronize { @active_interruptions.dup }
780
+ @interjections_mutex.synchronize { @active_interjections.dup }
786
781
  ]
787
782
  end
788
783
  first_error = nil
789
- interruptions.each do |active|
790
- active.close(AgentInterruptError.new("Agent was closed"))
784
+ interjections.each do |active|
785
+ active.close(AgentInterjectionError.new("Agent was closed"))
791
786
  end
792
787
  resources.each do |resource|
793
788
  resource.close if resource.respond_to?(:close)
@@ -829,45 +824,45 @@ module LittleGhost
829
824
  {message: input, **options}
830
825
  end
831
826
 
832
- def register_interruptions(interruptions)
827
+ def register_interjections(interjections)
833
828
  @close_mutex.synchronize do
834
829
  raise InvocationError, "Agent is closed" if @closed
835
830
 
836
- @interruptions_mutex.synchronize { @active_interruptions << interruptions }
831
+ @interjections_mutex.synchronize { @active_interjections << interjections }
837
832
  end
838
833
  end
839
834
 
840
- def unregister_interruptions(interruptions)
841
- @interruptions_mutex.synchronize { @active_interruptions.delete(interruptions) }
835
+ def unregister_interjections(interjections)
836
+ @interjections_mutex.synchronize { @active_interjections.delete(interjections) }
842
837
  end
843
838
 
844
- def interruption_message(interruption)
845
- source = interruption.message
839
+ def interjection_message(interjection)
840
+ source = interjection.message
846
841
  Message.new(
847
842
  role: :user,
848
843
  content: [
849
844
  Content::Text.new(text: <<~MESSAGE.strip),
850
- Agent interruption:
845
+ Agent interjection:
851
846
 
852
847
  Respond briefly in ordinary text before any tool calls, then continue the current task unless this
853
- interruption asks you to finish.
848
+ interjection asks you to finish.
854
849
  MESSAGE
855
850
  *source.content
856
851
  ],
857
- metadata: source.metadata.merge(interruption.metadata).merge(
858
- little_ghost_interruption_id: interruption.id,
859
- little_ghost_interruption_batch_key: interruption.batch_key,
860
- little_ghost_interruption_metadata: interruption.metadata
852
+ metadata: source.metadata.merge(interjection.metadata).merge(
853
+ little_ghost_interjection_id: interjection.id,
854
+ little_ghost_interjection_batch_key: interjection.batch_key,
855
+ little_ghost_interjection_metadata: interjection.metadata
861
856
  )
862
857
  )
863
858
  end
864
859
 
865
- def request_with_interruption(request, interruption)
860
+ def request_with_interjection(request, interjection)
866
861
  ModelRequest.new(
867
862
  messages: [
868
863
  *request.messages,
869
- *interruption.tickets.reject { |ticket| request_contains_interruption?(request, ticket) }
870
- .map { |ticket| interruption_message(ticket) }
864
+ *interjection.tickets.reject { |ticket| request_contains_interjection?(request, ticket) }
865
+ .map { |ticket| interjection_message(ticket) }
871
866
  ],
872
867
  tools: request.tools,
873
868
  settings: request.settings,
@@ -879,10 +874,10 @@ module LittleGhost
879
874
  )
880
875
  end
881
876
 
882
- def request_contains_interruption?(request, interruption)
877
+ def request_contains_interjection?(request, interjection)
883
878
  request.messages.any? do |message|
884
- (message.metadata[:little_ghost_interruption_id] ||
885
- message.metadata["little_ghost_interruption_id"]) == interruption.id
879
+ (message.metadata[:little_ghost_interjection_id] ||
880
+ message.metadata["little_ghost_interjection_id"]) == interjection.id
886
881
  end
887
882
  end
888
883
 
@@ -906,16 +901,16 @@ module LittleGhost
906
901
  template_paths:,
907
902
  events:,
908
903
  parent_operation_id:,
909
- interruptions:,
910
- interrupt_ready:
904
+ interjections:,
905
+ interject_ready:
911
906
  )
912
907
  started_at = monotonic_time
913
908
  operation_id = SecureRandom.uuid
914
909
  context.bind_agent_operation_id(operation_id)
915
910
  @assembly_transitions_mutex.synchronize { @assembly_transition = nil }
916
- interruptions.bind(operation_id, target_operation_id: parent_operation_id)
917
- register_interruptions(interruptions)
918
- interrupt_ready&.call
911
+ interjections.bind(operation_id, target_operation_id: parent_operation_id)
912
+ register_interjections(interjections)
913
+ interject_ready&.call
919
914
  agent_handle = start_instrumentation(
920
915
  :agent,
921
916
  parent: parent_operation_id || active_instrumentation_parent,
@@ -946,7 +941,7 @@ module LittleGhost
946
941
  )
947
942
  begin
948
943
  context.check!
949
- response, interrupted = invoke_model(
944
+ response, interjected = invoke_model(
950
945
  messages,
951
946
  context,
952
947
  settings,
@@ -954,7 +949,7 @@ module LittleGhost
954
949
  events,
955
950
  parent_operation_id: turn_operation_id,
956
951
  structured_result_repair_due:,
957
- interruptions:
952
+ interjections:
958
953
  )
959
954
  messages.reject! { |message| tool_companion_message?(message) }
960
955
  messages << response.message
@@ -969,12 +964,12 @@ module LittleGhost
969
964
  )
970
965
  unless validation_error
971
966
  messages[-1] = redact_structured_result_message(response.message)
972
- unless interruptions.finish
967
+ unless interjections.finish
973
968
  context.checkpoint(messages)
974
969
  finish_instrumentation(
975
970
  turn_handle,
976
971
  operation_id: turn_operation_id,
977
- outcome: :interrupted,
972
+ outcome: :interjected,
978
973
  turn: turn + 1
979
974
  )
980
975
  next
@@ -1029,20 +1024,20 @@ module LittleGhost
1029
1024
  raise OutputLimitError, "The model stopped before completing its response"
1030
1025
  end
1031
1026
 
1032
- if interrupted || !@structured_output_strategy
1033
- unless interruptions.finish
1027
+ if interjected || !@structured_output_strategy
1028
+ unless interjections.finish
1034
1029
  context.checkpoint(messages)
1035
1030
  finish_instrumentation(
1036
1031
  turn_handle,
1037
1032
  operation_id: turn_operation_id,
1038
- outcome: :interrupted,
1033
+ outcome: :interjected,
1039
1034
  turn: turn + 1
1040
1035
  )
1041
1036
  next
1042
1037
  end
1043
1038
  end
1044
1039
 
1045
- if @structured_output_strategy && !interrupted
1040
+ if @structured_output_strategy && !interjected
1046
1041
  validation_error = if @structured_output_strategy.provider?
1047
1042
  capture_structured_result(response.message.text, context)
1048
1043
  else
@@ -1050,12 +1045,12 @@ module LittleGhost
1050
1045
  end
1051
1046
  unless validation_error
1052
1047
  messages[-1] = redact_structured_result_message(response.message)
1053
- unless interruptions.finish
1048
+ unless interjections.finish
1054
1049
  context.checkpoint(messages)
1055
1050
  finish_instrumentation(
1056
1051
  turn_handle,
1057
1052
  operation_id: turn_operation_id,
1058
- outcome: :interrupted,
1053
+ outcome: :interjected,
1059
1054
  turn: turn + 1
1060
1055
  )
1061
1056
  next
@@ -1263,10 +1258,10 @@ module LittleGhost
1263
1258
  turn,
1264
1259
  events,
1265
1260
  parent_operation_id:,
1266
- interruptions:,
1261
+ interjections:,
1267
1262
  structured_result_repair_due: false,
1268
1263
  recovery_attempt: 0,
1269
- interruption: nil
1264
+ interjection: nil
1270
1265
  )
1271
1266
  started_at = monotonic_time
1272
1267
  operation_id = SecureRandom.uuid
@@ -1283,9 +1278,9 @@ module LittleGhost
1283
1278
  cancellation_token: context.cancellation_token,
1284
1279
  deadline: context.deadline
1285
1280
  )
1286
- interruption ||= interruptions.deliver
1287
- if interruption
1288
- context.activate_interruption(metadata: interruption.metadata, ids: interruption.interruption_ids)
1281
+ interjection ||= interjections.deliver
1282
+ if interjection
1283
+ context.activate_interjection(metadata: interjection.metadata, ids: interjection.interjection_ids)
1289
1284
  end
1290
1285
  decision = run_callbacks(
1291
1286
  :before_model,
@@ -1294,11 +1289,11 @@ module LittleGhost
1294
1289
  )
1295
1290
  apply_cancellation_decision!(decision)
1296
1291
  request = replacement_value(decision, :request, request)
1297
- interruption_delivered = interruption&.tickets&.any? do |ticket|
1298
- !request_contains_interruption?(request, ticket)
1292
+ interjection_delivered = interjection&.tickets&.any? do |ticket|
1293
+ !request_contains_interjection?(request, ticket)
1299
1294
  end
1300
- if interruption_delivered
1301
- request = request_with_interruption(request, interruption)
1295
+ if interjection_delivered
1296
+ request = request_with_interjection(request, interjection)
1302
1297
  end
1303
1298
  messages.replace(request.messages)
1304
1299
  context.checkpoint(messages)
@@ -1313,20 +1308,20 @@ module LittleGhost
1313
1308
  model_settings: request.settings,
1314
1309
  **model_attributes
1315
1310
  )
1316
- if interruption_delivered
1317
- interruption.tickets.each do |ticket|
1311
+ if interjection_delivered
1312
+ interjection.tickets.each do |ticket|
1318
1313
  instrument(
1319
- :agent_interrupt_delivered,
1314
+ :agent_interjection_delivered,
1320
1315
  parent_operation_id: operation_id,
1321
- interruption_id: ticket.id,
1322
- event_kind: :interrupt
1316
+ interjection_id: ticket.id,
1317
+ event_kind: :interjection
1323
1318
  )
1324
1319
  end
1325
1320
  emit(
1326
1321
  events,
1327
- :agent_interrupt_delivered,
1328
- interruption_ids: interruption.interruption_ids,
1329
- batch_key: interruption.batch_key
1322
+ :agent_interjection_delivered,
1323
+ interjection_ids: interjection.interjection_ids,
1324
+ batch_key: interjection.batch_key
1330
1325
  )
1331
1326
  end
1332
1327
  emit(events, :model_start, turn: turn)
@@ -1364,21 +1359,21 @@ module LittleGhost
1364
1359
  decision = run_callbacks(:after_model, {request: request, response: response, turn: turn}, context: context)
1365
1360
  apply_cancellation_decision!(decision)
1366
1361
  response = replacement_value(decision, :response, response)
1367
- interruptions.resolve(
1368
- interruption,
1369
- AgentInterruptions::Response.new(
1362
+ interjections.resolve(
1363
+ interjection,
1364
+ AgentInterjections::Result.new(
1370
1365
  text: response.message.text,
1371
1366
  tool_calls: response.message.content.any? { |content| content.is_a?(Content::ToolUse) },
1372
- interruption_ids: interruption&.interruption_ids || [],
1373
- batch_key: interruption&.batch_key
1367
+ interjection_ids: interjection&.interjection_ids || [],
1368
+ batch_key: interjection&.batch_key
1374
1369
  )
1375
1370
  )
1376
- interruption&.tickets&.each do |ticket|
1371
+ interjection&.tickets&.each do |ticket|
1377
1372
  instrument(
1378
- :agent_interrupt_responded,
1373
+ :agent_interjection_responded,
1379
1374
  parent_operation_id: operation_id,
1380
- interruption_id: ticket.id,
1381
- event_kind: :interrupt,
1375
+ interjection_id: ticket.id,
1376
+ event_kind: :interjection,
1382
1377
  diagnostic: {output: response.message.text}
1383
1378
  )
1384
1379
  end
@@ -1427,7 +1422,7 @@ module LittleGhost
1427
1422
  repair: structured_result_repair_due
1428
1423
  )
1429
1424
  )
1430
- [response, !interruption.nil?]
1425
+ [response, !interjection.nil?]
1431
1426
  rescue => error
1432
1427
  finish_instrumentation(
1433
1428
  model_handle,
@@ -1463,8 +1458,8 @@ module LittleGhost
1463
1458
  parent_operation_id:,
1464
1459
  structured_result_repair_due:,
1465
1460
  recovery_attempt: recovery_attempt + 1,
1466
- interruptions:,
1467
- interruption:
1461
+ interjections:,
1462
+ interjection:
1468
1463
  )
1469
1464
  end
1470
1465
  end