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.
- checksums.yaml +4 -4
- data/README.md +68 -84
- data/docs/guides/assemblies.md +286 -0
- data/docs/guides/core_concepts.md +126 -231
- data/docs/guides/getting_started.md +114 -87
- data/docs/guides/production.md +187 -0
- data/docs/guides/prompt_views.md +132 -0
- data/lib/little_ghost/ag_ui/adapter.rb +3 -3
- data/lib/little_ghost/agent/delegation.rb +1 -1
- data/lib/little_ghost/agent.rb +167 -172
- data/lib/little_ghost/{agent_interruptions.rb → agent_interjections.rb} +12 -12
- data/lib/little_ghost/assembly.rb +55 -21
- data/lib/little_ghost/assembly_builder.rb +40 -2
- data/lib/little_ghost/assembly_execution.rb +87 -4
- data/lib/little_ghost/configuration.rb +263 -39
- data/lib/little_ghost/content.rb +5 -5
- data/lib/little_ghost/data_map.rb +209 -0
- data/lib/little_ghost/errors.rb +2 -2
- data/lib/little_ghost/execution.rb +32 -32
- data/lib/little_ghost/graph.rb +22 -3
- data/lib/little_ghost/message.rb +4 -4
- data/lib/little_ghost/model_resolver.rb +2 -2
- data/lib/little_ghost/prompt_resolver.rb +2 -0
- data/lib/little_ghost/run.rb +87 -49
- data/lib/little_ghost/run_context.rb +33 -20
- data/lib/little_ghost/runtime/hook.rb +3 -3
- data/lib/little_ghost/runtime.rb +71 -31
- data/lib/little_ghost/session.rb +12 -23
- data/lib/little_ghost/session_store.rb +9 -5
- data/lib/little_ghost/session_stores/agent_core_memory.rb +64 -56
- data/lib/little_ghost/session_stores/filesystem.rb +261 -0
- data/lib/little_ghost/session_stores/memory.rb +7 -0
- data/lib/little_ghost/subagents/manager.rb +42 -42
- data/lib/little_ghost/swarm.rb +13 -5
- data/lib/little_ghost/tool.rb +56 -14
- data/lib/little_ghost/tools/write_todos.rb +6 -1
- data/lib/little_ghost/tracing/open_telemetry.rb +1 -1
- data/lib/little_ghost/version.rb +1 -1
- data/lib/little_ghost/workflow.rb +30 -21
- data/lib/little_ghost.rb +29 -25
- metadata +7 -2
data/lib/little_ghost/agent.rb
CHANGED
|
@@ -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
|
-
#
|
|
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 :
|
|
25
|
-
#
|
|
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
|
|
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
|
-
#
|
|
35
|
-
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
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
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
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
|
-
#
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
-
#
|
|
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
|
-
#
|
|
165
|
-
# LittleGhost::StructuredResultError is raised
|
|
166
|
-
#
|
|
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
|
-
#
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
#
|
|
477
|
-
#
|
|
478
|
-
#
|
|
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
|
-
@
|
|
503
|
-
@
|
|
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
|
-
@
|
|
539
|
-
@
|
|
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
|
|
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
|
-
#
|
|
608
|
-
# +batch_key+; tool calls may continue after this
|
|
609
|
-
# methods rather than the
|
|
610
|
-
def
|
|
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
|
-
|
|
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, "
|
|
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, "
|
|
622
|
+
raise ArgumentError, "interject message content must contain only text, images, or documents"
|
|
628
623
|
end
|
|
629
624
|
|
|
630
|
-
|
|
625
|
+
interjections = @interjections_mutex.synchronize do
|
|
631
626
|
active = if target_operation_id
|
|
632
|
-
@
|
|
627
|
+
@active_interjections.select { |candidate| candidate.target_operation_id == target_operation_id }
|
|
633
628
|
else
|
|
634
|
-
@
|
|
629
|
+
@active_interjections
|
|
635
630
|
end
|
|
636
631
|
if active.empty?
|
|
637
|
-
raise
|
|
632
|
+
raise AgentInterjectionError, "Agent is not currently running"
|
|
638
633
|
end
|
|
639
634
|
if active.length > 1
|
|
640
|
-
raise
|
|
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] =
|
|
647
|
-
ticket =
|
|
641
|
+
options[:id] = interjection_id unless interjection_id.nil?
|
|
642
|
+
ticket = interjections.enqueue(message, **options)
|
|
648
643
|
instrument(
|
|
649
|
-
:
|
|
650
|
-
parent_operation_id:
|
|
651
|
-
|
|
652
|
-
event_kind: :
|
|
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
|
-
|
|
652
|
+
interjections.release(ticket)
|
|
658
653
|
response
|
|
659
654
|
rescue => error
|
|
660
|
-
|
|
655
|
+
interjections.release(ticket, withdraw: true)
|
|
661
656
|
instrument(
|
|
662
|
-
:
|
|
663
|
-
parent_operation_id:
|
|
664
|
-
|
|
665
|
-
event_kind: :
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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
|
-
|
|
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
|
-
|
|
734
|
-
|
|
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
|
-
|
|
748
|
-
|
|
742
|
+
interjections:,
|
|
743
|
+
interject_ready:
|
|
749
744
|
)
|
|
750
745
|
end
|
|
751
746
|
rescue => error
|
|
752
|
-
|
|
747
|
+
interjections.close(error)
|
|
753
748
|
raise
|
|
754
749
|
ensure
|
|
755
|
-
|
|
756
|
-
|
|
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,
|
|
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,
|
|
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
|
-
@
|
|
780
|
+
@interjections_mutex.synchronize { @active_interjections.dup }
|
|
786
781
|
]
|
|
787
782
|
end
|
|
788
783
|
first_error = nil
|
|
789
|
-
|
|
790
|
-
active.close(
|
|
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
|
|
827
|
+
def register_interjections(interjections)
|
|
833
828
|
@close_mutex.synchronize do
|
|
834
829
|
raise InvocationError, "Agent is closed" if @closed
|
|
835
830
|
|
|
836
|
-
@
|
|
831
|
+
@interjections_mutex.synchronize { @active_interjections << interjections }
|
|
837
832
|
end
|
|
838
833
|
end
|
|
839
834
|
|
|
840
|
-
def
|
|
841
|
-
@
|
|
835
|
+
def unregister_interjections(interjections)
|
|
836
|
+
@interjections_mutex.synchronize { @active_interjections.delete(interjections) }
|
|
842
837
|
end
|
|
843
838
|
|
|
844
|
-
def
|
|
845
|
-
source =
|
|
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
|
|
845
|
+
Agent interjection:
|
|
851
846
|
|
|
852
847
|
Respond briefly in ordinary text before any tool calls, then continue the current task unless this
|
|
853
|
-
|
|
848
|
+
interjection asks you to finish.
|
|
854
849
|
MESSAGE
|
|
855
850
|
*source.content
|
|
856
851
|
],
|
|
857
|
-
metadata: source.metadata.merge(
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
|
860
|
+
def request_with_interjection(request, interjection)
|
|
866
861
|
ModelRequest.new(
|
|
867
862
|
messages: [
|
|
868
863
|
*request.messages,
|
|
869
|
-
*
|
|
870
|
-
.map { |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
|
|
877
|
+
def request_contains_interjection?(request, interjection)
|
|
883
878
|
request.messages.any? do |message|
|
|
884
|
-
(message.metadata[:
|
|
885
|
-
message.metadata["
|
|
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
|
-
|
|
910
|
-
|
|
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
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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: :
|
|
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
|
|
1033
|
-
unless
|
|
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: :
|
|
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 && !
|
|
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
|
|
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: :
|
|
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
|
-
|
|
1261
|
+
interjections:,
|
|
1267
1262
|
structured_result_repair_due: false,
|
|
1268
1263
|
recovery_attempt: 0,
|
|
1269
|
-
|
|
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
|
-
|
|
1287
|
-
if
|
|
1288
|
-
context.
|
|
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
|
-
|
|
1298
|
-
!
|
|
1292
|
+
interjection_delivered = interjection&.tickets&.any? do |ticket|
|
|
1293
|
+
!request_contains_interjection?(request, ticket)
|
|
1299
1294
|
end
|
|
1300
|
-
if
|
|
1301
|
-
request =
|
|
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
|
|
1317
|
-
|
|
1311
|
+
if interjection_delivered
|
|
1312
|
+
interjection.tickets.each do |ticket|
|
|
1318
1313
|
instrument(
|
|
1319
|
-
:
|
|
1314
|
+
:agent_interjection_delivered,
|
|
1320
1315
|
parent_operation_id: operation_id,
|
|
1321
|
-
|
|
1322
|
-
event_kind: :
|
|
1316
|
+
interjection_id: ticket.id,
|
|
1317
|
+
event_kind: :interjection
|
|
1323
1318
|
)
|
|
1324
1319
|
end
|
|
1325
1320
|
emit(
|
|
1326
1321
|
events,
|
|
1327
|
-
:
|
|
1328
|
-
|
|
1329
|
-
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
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
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
|
-
|
|
1373
|
-
batch_key:
|
|
1367
|
+
interjection_ids: interjection&.interjection_ids || [],
|
|
1368
|
+
batch_key: interjection&.batch_key
|
|
1374
1369
|
)
|
|
1375
1370
|
)
|
|
1376
|
-
|
|
1371
|
+
interjection&.tickets&.each do |ticket|
|
|
1377
1372
|
instrument(
|
|
1378
|
-
:
|
|
1373
|
+
:agent_interjection_responded,
|
|
1379
1374
|
parent_operation_id: operation_id,
|
|
1380
|
-
|
|
1381
|
-
event_kind: :
|
|
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, !
|
|
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
|
-
|
|
1467
|
-
|
|
1461
|
+
interjections:,
|
|
1462
|
+
interjection:
|
|
1468
1463
|
)
|
|
1469
1464
|
end
|
|
1470
1465
|
end
|