little_ghost 0.2.0 → 0.3.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 +19 -5
- data/docs/guides/core_concepts.md +167 -38
- data/docs/guides/getting_started.md +8 -4
- data/lib/little_ghost/agent/delegation.rb +34 -7
- data/lib/little_ghost/agent/tool_loop.rb +2 -1
- data/lib/little_ghost/agent.rb +115 -156
- data/lib/little_ghost/agent_builder.rb +20 -4
- data/lib/little_ghost/agent_factory.rb +3 -0
- data/lib/little_ghost/assembly.rb +311 -0
- data/lib/little_ghost/assembly_builder.rb +459 -0
- data/lib/little_ghost/assembly_execution.rb +452 -0
- data/lib/little_ghost/errors.rb +8 -0
- data/lib/little_ghost/execution.rb +206 -0
- data/lib/little_ghost/graph.rb +911 -0
- data/lib/little_ghost/run.rb +120 -32
- data/lib/little_ghost/run_result.rb +22 -11
- data/lib/little_ghost/runtime/hook.rb +7 -2
- data/lib/little_ghost/runtime.rb +64 -6
- data/lib/little_ghost/sandbox.rb +1 -1
- data/lib/little_ghost/support/executor.rb +14 -2
- data/lib/little_ghost/support/loader.rb +2 -2
- data/lib/little_ghost/support.rb +15 -3
- data/lib/little_ghost/swarm.rb +431 -0
- data/lib/little_ghost/tool.rb +32 -6
- data/lib/little_ghost/tracing/open_telemetry.rb +13 -2
- data/lib/little_ghost/unrestricted_sandbox.rb +1 -1
- data/lib/little_ghost/version.rb +1 -1
- data/lib/little_ghost/workflow.rb +198 -73
- data/lib/little_ghost.rb +11 -4
- metadata +11 -4
data/lib/little_ghost/agent.rb
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "securerandom"
|
|
4
|
+
require_relative "assembly"
|
|
4
5
|
require_relative "support/output_truncation"
|
|
5
6
|
require_relative "tool_execution"
|
|
6
7
|
|
|
7
8
|
module LittleGhost
|
|
8
|
-
#
|
|
9
|
-
# Each subclass describes one application role with an inheritable Ruby
|
|
9
|
+
# Defines one reusable model-driven behavior with prompts, tools, and limits.
|
|
10
|
+
# Each Agent subclass describes one application role with an inheritable Ruby
|
|
11
|
+
# DSL. It can answer, stream, call tools, and delegate work.
|
|
10
12
|
#
|
|
11
|
-
# A customer support agent can look up an account itself and give longer
|
|
12
|
-
# to a research specialist:
|
|
13
|
+
# A customer support agent can look up an account itself and give longer
|
|
14
|
+
# investigations to a research specialist:
|
|
13
15
|
#
|
|
14
16
|
# class ResearchAgent < LittleGhost::Agent
|
|
15
17
|
# description "Researches transfer failures"
|
|
@@ -48,7 +50,7 @@ module LittleGhost
|
|
|
48
50
|
# Tool failures are sanitized before returning to the model, diagnostic
|
|
49
51
|
# capture can be disabled for sensitive agents, and cancellation, deadlines,
|
|
50
52
|
# and cleanup failures remain framework control flow.
|
|
51
|
-
class Agent
|
|
53
|
+
class Agent < Assembly
|
|
52
54
|
DEFAULT_SYSTEM_PROMPT = "You are a helpful agent." # :nodoc:
|
|
53
55
|
DEFAULT_MAX_TOOL_RESULT_TOKENS = 10_000 # :nodoc:
|
|
54
56
|
MAX_STRUCTURED_RESULT_BYTES = 1_000_000 # :nodoc:
|
|
@@ -64,11 +66,13 @@ module LittleGhost
|
|
|
64
66
|
before_model after_model after_model_error
|
|
65
67
|
before_tool after_tool
|
|
66
68
|
].freeze # :nodoc:
|
|
69
|
+
ExecutedTool = Data.define(:result, :companion_content) do # :nodoc:
|
|
70
|
+
def status = result.status
|
|
71
|
+
def content = result.content
|
|
72
|
+
end
|
|
67
73
|
|
|
68
74
|
extend Support::ClassAttributes
|
|
69
75
|
|
|
70
|
-
class_attribute :agent_id_value
|
|
71
|
-
class_attribute :description_value
|
|
72
76
|
class_attribute :model_value
|
|
73
77
|
class_attribute :limits_value, default: {}
|
|
74
78
|
class_attribute :result_schema_value
|
|
@@ -81,27 +85,6 @@ module LittleGhost
|
|
|
81
85
|
class_attribute :callback_values, default: Support::Callbacks.new(*CALLBACKS)
|
|
82
86
|
|
|
83
87
|
class << self
|
|
84
|
-
# Executes +message+ through a fresh standalone entrypoint and returns the
|
|
85
|
-
# completed LittleGhost::Run. Invocation +options+ are forwarded to #ask.
|
|
86
|
-
#
|
|
87
|
-
# Create an instance explicitly when reusing a Runtime or streaming events.
|
|
88
|
-
def ask(message, **options)
|
|
89
|
-
new.ask(message, **options)
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Streams +message+ through a fresh standalone entrypoint and returns an
|
|
93
|
-
# Enumerator of LittleGhost::StreamEvent objects. Invocation +options+
|
|
94
|
-
# are forwarded to #stream_ask.
|
|
95
|
-
#
|
|
96
|
-
# Create an instance explicitly when reusing one Runtime across calls.
|
|
97
|
-
def stream_ask(message, **options)
|
|
98
|
-
stream = nil
|
|
99
|
-
Enumerator.new do |events|
|
|
100
|
-
stream ||= new.stream_ask(message, **options)
|
|
101
|
-
stream.each { |event| events << event }
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
88
|
# :call-seq:
|
|
106
89
|
# agent_id() -> String
|
|
107
90
|
# agent_id(value) -> String
|
|
@@ -110,9 +93,9 @@ module LittleGhost
|
|
|
110
93
|
# Named subclasses derive it from their underscored class name without an
|
|
111
94
|
# +Agent+ suffix; passing +value+ replaces that default.
|
|
112
95
|
def agent_id(*values)
|
|
113
|
-
return
|
|
96
|
+
return assembly_id if values.empty?
|
|
114
97
|
|
|
115
|
-
|
|
98
|
+
assembly_id(values.fetch(0))
|
|
116
99
|
end
|
|
117
100
|
|
|
118
101
|
# The underscored, namespace-aware path used for conventional prompt lookup.
|
|
@@ -122,17 +105,6 @@ module LittleGhost
|
|
|
122
105
|
parts.reject(&:empty?).map { |part| underscore(part) }.join("/")
|
|
123
106
|
end
|
|
124
107
|
|
|
125
|
-
# :call-seq:
|
|
126
|
-
# description() -> String
|
|
127
|
-
# description(value) -> String
|
|
128
|
-
#
|
|
129
|
-
# The human-readable description shown when this agent is delegated.
|
|
130
|
-
def description(*values)
|
|
131
|
-
return description_value.to_s if values.empty?
|
|
132
|
-
|
|
133
|
-
self.description_value = values.fetch(0).to_s
|
|
134
|
-
end
|
|
135
|
-
|
|
136
108
|
# :call-seq:
|
|
137
109
|
# model() -> String, Symbol, Hash, Proc, nil
|
|
138
110
|
# model(role_or_target) -> String, Symbol
|
|
@@ -489,11 +461,6 @@ module LittleGhost
|
|
|
489
461
|
end
|
|
490
462
|
end
|
|
491
463
|
|
|
492
|
-
def default_agent_id
|
|
493
|
-
value = name.to_s.split("::").last.to_s.gsub(/Agent\z/, "").gsub(/([a-z\d])([A-Z])/, "\\1_\\2").downcase
|
|
494
|
-
(value.empty? ? "agent" : value).freeze
|
|
495
|
-
end
|
|
496
|
-
|
|
497
464
|
def underscore(value)
|
|
498
465
|
value.gsub(/([a-z\d])([A-Z])/, "\\1_\\2").downcase
|
|
499
466
|
end
|
|
@@ -526,11 +493,9 @@ module LittleGhost
|
|
|
526
493
|
workspace: nil,
|
|
527
494
|
sandbox: nil
|
|
528
495
|
)
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
@workspace = workspace
|
|
533
|
-
@sandbox = sandbox
|
|
496
|
+
standalone = model.nil? && run.nil?
|
|
497
|
+
super(run:, runtime:, workspace:, sandbox:, standalone:)
|
|
498
|
+
if standalone
|
|
534
499
|
@owns_resources = true
|
|
535
500
|
@closed = false
|
|
536
501
|
@close_mutex = Mutex.new
|
|
@@ -572,6 +537,10 @@ module LittleGhost
|
|
|
572
537
|
@exclusive_tools_mutex = Mutex.new
|
|
573
538
|
@interruptions_mutex = Mutex.new
|
|
574
539
|
@active_interruptions = []
|
|
540
|
+
@assembly_transitions_mutex = Mutex.new
|
|
541
|
+
@assembly_transitions = {}
|
|
542
|
+
@assembly_tool_batch_sizes = {}
|
|
543
|
+
@assembly_transition = nil
|
|
575
544
|
raise ArgumentError, "max_turns must be at least 1" if @max_turns < 1
|
|
576
545
|
raise ArgumentError, "max_tool_calls must be at least 1" if @max_tool_calls < 1
|
|
577
546
|
raise ArgumentError, "max_tool_result_tokens must be at least 1" if @max_tool_result_tokens < 1
|
|
@@ -590,34 +559,21 @@ module LittleGhost
|
|
|
590
559
|
execute_tools(tool_uses, context, events, parent_operation_id:, parent_trace_context:)
|
|
591
560
|
end
|
|
592
561
|
|
|
593
|
-
def
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
end
|
|
562
|
+
def request_assembly_transition(value, context:) # :nodoc:
|
|
563
|
+
@assembly_transitions_mutex.synchronize do
|
|
564
|
+
unless @assembly_tool_batch_sizes[context] == 1
|
|
565
|
+
raise ToolError, "An assembly transition must be the only tool call in a model response"
|
|
566
|
+
end
|
|
567
|
+
if @assembly_transitions.key?(context)
|
|
568
|
+
raise ProtocolError, "Multiple assembly transitions were requested in one agent turn"
|
|
569
|
+
end
|
|
602
570
|
|
|
603
|
-
|
|
604
|
-
#
|
|
605
|
-
# A standalone agent returns a LittleGhost::Run. An agent built inside a run
|
|
606
|
-
# returns its LittleGhost::RunResult.
|
|
607
|
-
def call(input = nil, **options)
|
|
608
|
-
return build_run(entrypoint_payload(input, options)).call if @standalone
|
|
609
|
-
|
|
610
|
-
result = nil
|
|
611
|
-
stream(input, **options).each do |event|
|
|
612
|
-
result = event.data[:result] if event.type == :invocation_stop
|
|
571
|
+
@assembly_transitions[context] = value
|
|
613
572
|
end
|
|
614
|
-
|
|
573
|
+
value
|
|
615
574
|
end
|
|
616
575
|
|
|
617
|
-
|
|
618
|
-
def ask(message, **options)
|
|
619
|
-
call(message, **options)
|
|
620
|
-
end
|
|
576
|
+
attr_reader :assembly_transition # :nodoc:
|
|
621
577
|
|
|
622
578
|
# Adds +message+ to one active invocation and waits for its ordinary text reply.
|
|
623
579
|
#
|
|
@@ -738,7 +694,7 @@ module LittleGhost
|
|
|
738
694
|
interruption_ids: [],
|
|
739
695
|
interrupt_ready: nil
|
|
740
696
|
)
|
|
741
|
-
if
|
|
697
|
+
if standalone?
|
|
742
698
|
raise ArgumentError, "input is required" if input.nil?
|
|
743
699
|
|
|
744
700
|
return build_run(entrypoint_payload(input, {
|
|
@@ -746,7 +702,8 @@ module LittleGhost
|
|
|
746
702
|
context:,
|
|
747
703
|
settings:,
|
|
748
704
|
template_paths:,
|
|
749
|
-
deadline_at: deadline
|
|
705
|
+
deadline_at: deadline,
|
|
706
|
+
cancellation_token:
|
|
750
707
|
}.compact)).each
|
|
751
708
|
end
|
|
752
709
|
|
|
@@ -801,69 +758,6 @@ module LittleGhost
|
|
|
801
758
|
end
|
|
802
759
|
end
|
|
803
760
|
|
|
804
|
-
# Streams +message+ through the standalone or run-scoped agent.
|
|
805
|
-
# Standalone +options+ become Invocation fields; run-scoped options are
|
|
806
|
-
# forwarded to #stream.
|
|
807
|
-
def stream_ask(message, **options)
|
|
808
|
-
if @standalone
|
|
809
|
-
options[:deadline_at] = options.delete(:deadline) if options.key?(:deadline)
|
|
810
|
-
stream = nil
|
|
811
|
-
return Enumerator.new do |events|
|
|
812
|
-
stream ||= build_run(entrypoint_payload(message, options)).each
|
|
813
|
-
stream.each { |event| events << event }
|
|
814
|
-
end
|
|
815
|
-
end
|
|
816
|
-
|
|
817
|
-
stream(message, **options)
|
|
818
|
-
end
|
|
819
|
-
|
|
820
|
-
# Exposes this agent as a Tool instance.
|
|
821
|
-
#
|
|
822
|
-
# By default, each call starts with empty conversational history. Set
|
|
823
|
-
# <tt>preserve_context: true</tt> to retain history serially between calls.
|
|
824
|
-
def as_tool(name: self.class.agent_id, description: self.class.description, preserve_context: false)
|
|
825
|
-
agent = self
|
|
826
|
-
description = "Delegate a task to #{name}." if description.to_s.empty?
|
|
827
|
-
mutex = Mutex.new
|
|
828
|
-
retained_history = []
|
|
829
|
-
tool_class = Tool.define(
|
|
830
|
-
name: name,
|
|
831
|
-
description: description,
|
|
832
|
-
input_schema: {
|
|
833
|
-
type: "object",
|
|
834
|
-
properties: {input: {type: "string"}},
|
|
835
|
-
required: ["input"],
|
|
836
|
-
additionalProperties: false
|
|
837
|
-
}
|
|
838
|
-
) do |input, context: nil|
|
|
839
|
-
invocation = lambda do
|
|
840
|
-
result = agent.call(
|
|
841
|
-
input.fetch("input"),
|
|
842
|
-
history: preserve_context ? retained_history : [],
|
|
843
|
-
context: context&.state || {},
|
|
844
|
-
cancellation_token: context&.cancellation_token || Support::CancellationToken.new,
|
|
845
|
-
interruption_metadata: context&.interruption_metadata,
|
|
846
|
-
interruption_ids: context&.interruption_ids || [],
|
|
847
|
-
deadline: context&.deadline,
|
|
848
|
-
parent_operation_id: run&.operation_id
|
|
849
|
-
)
|
|
850
|
-
retained_history.replace(result.messages.reject { |message| message.role == :system }) if preserve_context
|
|
851
|
-
result.structured? ? result.structured_result.value : result.text
|
|
852
|
-
end
|
|
853
|
-
preserve_context ? mutex.synchronize(&invocation) : invocation.call
|
|
854
|
-
end
|
|
855
|
-
tool_class.define_method(:close) { agent.close }
|
|
856
|
-
binding = Tool::Binding.new(
|
|
857
|
-
agent: self,
|
|
858
|
-
run:,
|
|
859
|
-
runtime:,
|
|
860
|
-
model:,
|
|
861
|
-
workspace:,
|
|
862
|
-
sandbox:
|
|
863
|
-
)
|
|
864
|
-
tool_class.new(binding:)
|
|
865
|
-
end
|
|
866
|
-
|
|
867
761
|
# Materializes and freezes the prompt locals declared on the agent class.
|
|
868
762
|
def prompt_locals
|
|
869
763
|
self.class.prompt_local_resolvers.to_h do |name, resolver|
|
|
@@ -992,6 +886,17 @@ module LittleGhost
|
|
|
992
886
|
end
|
|
993
887
|
end
|
|
994
888
|
|
|
889
|
+
def consume_assembly_transition(context)
|
|
890
|
+
@assembly_transitions_mutex.synchronize { @assembly_transitions.delete(context) }
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
def with_assembly_tool_batch(context, size)
|
|
894
|
+
@assembly_transitions_mutex.synchronize { @assembly_tool_batch_sizes[context] = size }
|
|
895
|
+
yield
|
|
896
|
+
ensure
|
|
897
|
+
@assembly_transitions_mutex.synchronize { @assembly_tool_batch_sizes.delete(context) }
|
|
898
|
+
end
|
|
899
|
+
|
|
995
900
|
def execute(
|
|
996
901
|
input,
|
|
997
902
|
history:,
|
|
@@ -1007,6 +912,7 @@ module LittleGhost
|
|
|
1007
912
|
started_at = monotonic_time
|
|
1008
913
|
operation_id = SecureRandom.uuid
|
|
1009
914
|
context.bind_agent_operation_id(operation_id)
|
|
915
|
+
@assembly_transitions_mutex.synchronize { @assembly_transition = nil }
|
|
1010
916
|
interruptions.bind(operation_id, target_operation_id: parent_operation_id)
|
|
1011
917
|
register_interruptions(interruptions)
|
|
1012
918
|
interrupt_ready&.call
|
|
@@ -1050,6 +956,7 @@ module LittleGhost
|
|
|
1050
956
|
structured_result_repair_due:,
|
|
1051
957
|
interruptions:
|
|
1052
958
|
)
|
|
959
|
+
messages.reject! { |message| tool_companion_message?(message) }
|
|
1053
960
|
messages << response.message
|
|
1054
961
|
tool_uses = response.message.content.grep(Content::ToolUse)
|
|
1055
962
|
result_tool_uses = structured_result_tool_uses(tool_uses)
|
|
@@ -1241,17 +1148,61 @@ module LittleGhost
|
|
|
1241
1148
|
tool_call_count += tool_uses.length
|
|
1242
1149
|
raise ProtocolError, "The agent reached its maximum tool calls" if tool_call_count > @max_tool_calls
|
|
1243
1150
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1151
|
+
executed_tools = with_assembly_tool_batch(context, tool_uses.length) do
|
|
1152
|
+
dispatch_tools(
|
|
1153
|
+
tool_uses,
|
|
1154
|
+
context:,
|
|
1155
|
+
events:,
|
|
1156
|
+
parent_operation_id: turn_operation_id
|
|
1157
|
+
)
|
|
1158
|
+
end
|
|
1250
1159
|
messages << Message.new(
|
|
1251
1160
|
role: :tool,
|
|
1252
|
-
content:
|
|
1161
|
+
content: executed_tools.map(&:result)
|
|
1253
1162
|
)
|
|
1163
|
+
executed_tools.each do |executed_tool|
|
|
1164
|
+
next if executed_tool.companion_content.empty?
|
|
1165
|
+
|
|
1166
|
+
messages << Message.new(
|
|
1167
|
+
role: :user,
|
|
1168
|
+
content: executed_tool.companion_content,
|
|
1169
|
+
metadata: {transient: true, little_ghost_tool_companion: true}
|
|
1170
|
+
)
|
|
1171
|
+
end
|
|
1254
1172
|
context.checkpoint(messages)
|
|
1173
|
+
transition = consume_assembly_transition(context)
|
|
1174
|
+
if transition
|
|
1175
|
+
result = RunResult.new(
|
|
1176
|
+
message: response.message,
|
|
1177
|
+
stop_reason: :assembly_transition,
|
|
1178
|
+
usage: context.usage,
|
|
1179
|
+
messages: messages.freeze,
|
|
1180
|
+
state: context.state
|
|
1181
|
+
)
|
|
1182
|
+
decision = run_callbacks(:after_invocation, {result:}, context:)
|
|
1183
|
+
apply_cancellation_decision!(decision)
|
|
1184
|
+
result = replacement_value(decision, :result, result)
|
|
1185
|
+
@assembly_transition = transition
|
|
1186
|
+
context.checkpoint(result.messages)
|
|
1187
|
+
finish_instrumentation(
|
|
1188
|
+
turn_handle,
|
|
1189
|
+
operation_id: turn_operation_id,
|
|
1190
|
+
outcome: :completed,
|
|
1191
|
+
turn: turn + 1
|
|
1192
|
+
)
|
|
1193
|
+
metadata = model.details.to_h.merge(model_role: model.role)
|
|
1194
|
+
finish_instrumentation(
|
|
1195
|
+
agent_handle,
|
|
1196
|
+
outcome: :completed,
|
|
1197
|
+
duration_ms: duration_ms(started_at),
|
|
1198
|
+
stop_reason: result.stop_reason,
|
|
1199
|
+
operation_id:,
|
|
1200
|
+
diagnostic: {output: diagnostic_message(result.message)},
|
|
1201
|
+
**usage_attributes(result.usage)
|
|
1202
|
+
)
|
|
1203
|
+
emit(events, :invocation_stop, result:, metadata:)
|
|
1204
|
+
return result
|
|
1205
|
+
end
|
|
1255
1206
|
finish_instrumentation(
|
|
1256
1207
|
turn_handle,
|
|
1257
1208
|
operation_id: turn_operation_id,
|
|
@@ -1287,6 +1238,10 @@ module LittleGhost
|
|
|
1287
1238
|
end
|
|
1288
1239
|
raise ProtocolError, "The agent reached its maximum model turns"
|
|
1289
1240
|
rescue => error
|
|
1241
|
+
@assembly_transitions_mutex.synchronize do
|
|
1242
|
+
@assembly_transitions.delete(context)
|
|
1243
|
+
@assembly_tool_batch_sizes.delete(context)
|
|
1244
|
+
end
|
|
1290
1245
|
finish_instrumentation(
|
|
1291
1246
|
agent_handle,
|
|
1292
1247
|
operation_id:,
|
|
@@ -1560,7 +1515,7 @@ module LittleGhost
|
|
|
1560
1515
|
exception: diagnostic_tool_exception(tool, tool:)
|
|
1561
1516
|
}
|
|
1562
1517
|
)
|
|
1563
|
-
next result
|
|
1518
|
+
next ExecutedTool.new(result:, companion_content: [])
|
|
1564
1519
|
end
|
|
1565
1520
|
|
|
1566
1521
|
context.check!
|
|
@@ -1592,7 +1547,7 @@ module LittleGhost
|
|
|
1592
1547
|
exception: diagnostic_tool_exception(rejection, tool:)
|
|
1593
1548
|
}
|
|
1594
1549
|
)
|
|
1595
|
-
next result
|
|
1550
|
+
next ExecutedTool.new(result:, companion_content: [])
|
|
1596
1551
|
end
|
|
1597
1552
|
|
|
1598
1553
|
execution_context = ToolExecution.new(
|
|
@@ -1639,7 +1594,7 @@ module LittleGhost
|
|
|
1639
1594
|
exception: tool_error && diagnostic_tool_exception(tool_error, tool:)
|
|
1640
1595
|
}.compact
|
|
1641
1596
|
)
|
|
1642
|
-
result
|
|
1597
|
+
ExecutedTool.new(result:, companion_content: tool_result.companion_content)
|
|
1643
1598
|
rescue ToolError => error
|
|
1644
1599
|
result = build_tool_result(tool_use_id: tool_use.id, content: error.message, status: :error)
|
|
1645
1600
|
finish_instrumentation(
|
|
@@ -1655,7 +1610,7 @@ module LittleGhost
|
|
|
1655
1610
|
exception: diagnostic_tool_exception(error, tool:)
|
|
1656
1611
|
}
|
|
1657
1612
|
)
|
|
1658
|
-
result
|
|
1613
|
+
ExecutedTool.new(result:, companion_content: [])
|
|
1659
1614
|
rescue => error
|
|
1660
1615
|
finish_instrumentation(
|
|
1661
1616
|
tool_handle,
|
|
@@ -1671,16 +1626,16 @@ module LittleGhost
|
|
|
1671
1626
|
end
|
|
1672
1627
|
if tools.any?(&:exclusive?)
|
|
1673
1628
|
pairs.map do |tool_use, tool|
|
|
1674
|
-
|
|
1675
|
-
emit(events, :tool_stop, tool_use:, result:)
|
|
1676
|
-
|
|
1629
|
+
executed_tool = execution.call(tool_use, tool)
|
|
1630
|
+
emit(events, :tool_stop, tool_use:, result: executed_tool.result)
|
|
1631
|
+
executed_tool
|
|
1677
1632
|
end
|
|
1678
1633
|
else
|
|
1679
1634
|
@executor.map(
|
|
1680
1635
|
pairs,
|
|
1681
1636
|
cancellation_token: context.cancellation_token,
|
|
1682
|
-
on_result: lambda do |index,
|
|
1683
|
-
emit(events, :tool_stop, tool_use: tool_uses.fetch(index), result:)
|
|
1637
|
+
on_result: lambda do |index, executed_tool|
|
|
1638
|
+
emit(events, :tool_stop, tool_use: tool_uses.fetch(index), result: executed_tool.result)
|
|
1684
1639
|
end
|
|
1685
1640
|
) do |tool_use, tool|
|
|
1686
1641
|
execution.call(tool_use, tool)
|
|
@@ -2136,6 +2091,10 @@ module LittleGhost
|
|
|
2136
2091
|
value.respond_to?(:role) ? diagnostic_message(value) : value.to_s
|
|
2137
2092
|
end
|
|
2138
2093
|
|
|
2094
|
+
def tool_companion_message?(message)
|
|
2095
|
+
message.metadata[:little_ghost_tool_companion] || message.metadata["little_ghost_tool_companion"]
|
|
2096
|
+
end
|
|
2097
|
+
|
|
2139
2098
|
def diagnostic_tool_exception(error, tool:)
|
|
2140
2099
|
diagnostic_exception(error).merge(message: truncated_tool_result(error.message))
|
|
2141
2100
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module LittleGhost
|
|
4
|
-
class
|
|
4
|
+
class AgentFactory # :nodoc: all
|
|
5
5
|
class ActivityRelay
|
|
6
6
|
def initialize
|
|
7
7
|
@mutex = Mutex.new
|
|
@@ -92,8 +92,8 @@ module LittleGhost
|
|
|
92
92
|
|
|
93
93
|
def agent_tools(agent_class, run)
|
|
94
94
|
tools = []
|
|
95
|
-
agent_class.
|
|
96
|
-
child =
|
|
95
|
+
agent_class.assembly_tool_declarations.each do |declaration|
|
|
96
|
+
child = declared_assembly(declaration, run)
|
|
97
97
|
begin
|
|
98
98
|
tools << child.as_tool(
|
|
99
99
|
name: declaration.fetch(:name),
|
|
@@ -111,6 +111,17 @@ module LittleGhost
|
|
|
111
111
|
raise
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
+
def declared_assembly(declaration, run)
|
|
115
|
+
assembly_class = declaration.fetch(:assembly)
|
|
116
|
+
if assembly_class.is_a?(AssemblyDefinition) && assembly_class.kind == :agent
|
|
117
|
+
declared_agent(declaration.merge(agent: assembly_class.implementation), run)
|
|
118
|
+
elsif assembly_class <= Agent
|
|
119
|
+
declared_agent(declaration.merge(agent: assembly_class), run)
|
|
120
|
+
else
|
|
121
|
+
runtime.build_assembly(assembly_class, run:)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
114
125
|
def subagent_tools(agent_class, run, conversation_id:, delegation_activity:, agent_path:)
|
|
115
126
|
definitions = agent_class.subagent_declarations.map do |declaration|
|
|
116
127
|
Subagents::Definition.new(
|
|
@@ -159,7 +170,12 @@ module LittleGhost
|
|
|
159
170
|
end
|
|
160
171
|
|
|
161
172
|
def declared_agent(declaration, run, conversation_id: nil, agent_path: Subagents::AgentPath::ROOT)
|
|
162
|
-
|
|
173
|
+
reference = declaration.fetch(:agent)
|
|
174
|
+
agent_class = if reference.is_a?(AssemblyDefinition)
|
|
175
|
+
reference.implementation
|
|
176
|
+
else
|
|
177
|
+
@resolve_agent.call(reference)
|
|
178
|
+
end
|
|
163
179
|
build_agent(
|
|
164
180
|
agent_class, run:,
|
|
165
181
|
model: resolve(declaration[:model], run),
|