little_ghost 0.4.0 → 0.5.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 +28 -6
- data/docs/guides/assemblies.md +143 -17
- data/docs/guides/code_mode.md +275 -0
- data/docs/guides/core_concepts.md +46 -30
- data/docs/guides/getting_started.md +17 -6
- data/docs/guides/integrations.md +217 -0
- data/docs/guides/models_and_providers.md +125 -0
- data/docs/guides/production.md +81 -15
- data/docs/guides/prompt_views.md +14 -7
- data/docs/guides/sandboxing.md +282 -0
- data/docs/guides/skills.md +141 -0
- data/docs/guides/structured_outputs_and_content.md +135 -0
- data/docs/guides/tools.md +329 -0
- data/lib/little_ghost/ag_ui/adapter.rb +2 -2
- data/lib/little_ghost/agent/delegation.rb +2 -2
- data/lib/little_ghost/agent/skills.rb +6 -1
- data/lib/little_ghost/agent/tool_loop.rb +6 -2
- data/lib/little_ghost/agent.rb +248 -46
- data/lib/little_ghost/agent_builder.rb +61 -22
- data/lib/little_ghost/agent_stream_source.rb +262 -0
- data/lib/little_ghost/artifact.rb +182 -0
- data/lib/little_ghost/artifacts/presentation_budget.rb +44 -0
- data/lib/little_ghost/artifacts/workspace_store.rb +370 -0
- data/lib/little_ghost/artifacts.rb +11 -0
- data/lib/little_ghost/assembly.rb +63 -17
- data/lib/little_ghost/assembly_builder.rb +26 -26
- data/lib/little_ghost/assembly_execution.rb +23 -2
- data/lib/little_ghost/code_mode/broker.rb +164 -0
- data/lib/little_ghost/code_mode/catalog.rb +54 -0
- data/lib/little_ghost/code_mode/engine.rb +58 -0
- data/lib/little_ghost/code_mode/javascript/catalog.rb +118 -0
- data/lib/little_ghost/code_mode/javascript/client.rb +550 -0
- data/lib/little_ghost/code_mode/javascript/host.rb +347 -0
- data/lib/little_ghost/code_mode/javascript/session.rb +579 -0
- data/lib/little_ghost/code_mode/javascript_engine.rb +172 -0
- data/lib/little_ghost/code_mode/protocol.rb +78 -0
- data/lib/little_ghost/code_mode/ruby/catalog.rb +80 -0
- data/lib/little_ghost/code_mode/ruby/host.rb +102 -0
- data/lib/little_ghost/code_mode/ruby/session.rb +508 -0
- data/lib/little_ghost/code_mode/ruby_engine.rb +86 -0
- data/lib/little_ghost/code_mode/runtime.rb +335 -0
- data/lib/little_ghost/code_mode/session.rb +61 -0
- data/lib/little_ghost/code_mode/types.rb +58 -0
- data/lib/little_ghost/code_mode.rb +53 -0
- data/lib/little_ghost/configuration.rb +107 -12
- data/lib/little_ghost/content.rb +19 -8
- data/lib/little_ghost/data_map.rb +2 -2
- data/lib/little_ghost/errors.rb +26 -7
- data/lib/little_ghost/execution.rb +1 -2
- data/lib/little_ghost/graph.rb +367 -207
- data/lib/little_ghost/mcp/client.rb +487 -88
- data/lib/little_ghost/mcp/toolset.rb +210 -0
- data/lib/little_ghost/mcp/types.rb +216 -0
- data/lib/little_ghost/mcp.rb +3 -0
- data/lib/little_ghost/model_capabilities.rb +9 -6
- data/lib/little_ghost/model_request.rb +0 -12
- data/lib/little_ghost/model_resolver.rb +13 -3
- data/lib/little_ghost/model_response.rb +3 -7
- data/lib/little_ghost/network/authorizer_server.rb +162 -0
- data/lib/little_ghost/network/certificate_authority.rb +98 -0
- data/lib/little_ghost/network/envoy_config.rb +362 -0
- data/lib/little_ghost/network/envoy_gateway.rb +409 -0
- data/lib/little_ghost/network/external_gateway.rb +68 -0
- data/lib/little_ghost/network.rb +96 -0
- data/lib/little_ghost/prompt_resolver.rb +7 -7
- data/lib/little_ghost/provider_registry.rb +3 -3
- data/lib/little_ghost/providers/anthropic.rb +8 -1
- data/lib/little_ghost/providers/gemini.rb +10 -1
- data/lib/little_ghost/providers/vertex_ai.rb +6 -1
- data/lib/little_ghost/run.rb +83 -13
- data/lib/little_ghost/run_context.rb +9 -0
- data/lib/little_ghost/run_result.rb +0 -7
- data/lib/little_ghost/runtime/hook.rb +5 -0
- data/lib/little_ghost/runtime/hooks/artifacts.rb +338 -0
- data/lib/little_ghost/runtime.rb +102 -34
- data/lib/little_ghost/sandbox/capabilities.rb +68 -0
- data/lib/little_ghost/sandbox/environment_policy.rb +41 -0
- data/lib/little_ghost/sandbox/filesystem.rb +377 -0
- data/lib/little_ghost/sandbox/isolated_backend.rb +163 -0
- data/lib/little_ghost/sandbox/limits.rb +48 -0
- data/lib/little_ghost/sandbox/mount.rb +129 -0
- data/lib/little_ghost/sandbox/network_policy.rb +108 -0
- data/lib/little_ghost/sandbox/policy.rb +88 -0
- data/lib/little_ghost/sandbox/process_runner.rb +103 -0
- data/lib/little_ghost/sandbox/process_session.rb +335 -0
- data/lib/little_ghost/sandbox/scope.rb +304 -0
- data/lib/little_ghost/sandbox.rb +158 -32
- data/lib/little_ghost/sandboxes/bubblewrap.rb +351 -0
- data/lib/little_ghost/sandboxes/native.rb +51 -0
- data/lib/little_ghost/sandboxes/seatbelt.rb +261 -0
- data/lib/little_ghost/sandboxes/unrestricted.rb +241 -0
- data/lib/little_ghost/session.rb +30 -6
- data/lib/little_ghost/skills/catalog.rb +94 -14
- data/lib/little_ghost/skills/resource_root.rb +42 -0
- data/lib/little_ghost/skills/skill.rb +0 -3
- data/lib/little_ghost/stream_event.rb +8 -13
- data/lib/little_ghost/subagents/control_tool.rb +8 -0
- data/lib/little_ghost/subagents/manager.rb +12 -9
- data/lib/little_ghost/support/callbacks.rb +3 -1
- data/lib/little_ghost/support/content_capture.rb +3 -3
- data/lib/little_ghost/support/http_client.rb +2 -2
- data/lib/little_ghost/support/redactor.rb +1 -1
- data/lib/little_ghost/tool.rb +104 -53
- data/lib/little_ghost/tool_registry.rb +1 -1
- data/lib/little_ghost/tools/filesystem.rb +1 -1
- data/lib/little_ghost/tools/shell.rb +4 -3
- data/lib/little_ghost/version.rb +1 -1
- data/lib/little_ghost/workspace.rb +222 -8
- data/lib/little_ghost.rb +21 -12
- metadata +98 -2
- data/lib/little_ghost/unrestricted_sandbox.rb +0 -306
data/lib/little_ghost/agent.rb
CHANGED
|
@@ -45,10 +45,6 @@ module LittleGhost
|
|
|
45
45
|
# as skills, context management, loop detection, and delegation stay inactive
|
|
46
46
|
# until their DSL is used.
|
|
47
47
|
#
|
|
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
48
|
# Models may return text or locally validated structured data. LittleGhost
|
|
53
49
|
# hides unexpected Tool exception messages from the model. See
|
|
54
50
|
# Run[rdoc-ref:LittleGhost::Run] for outcomes, cancellation, and cleanup, and
|
|
@@ -69,9 +65,12 @@ module LittleGhost
|
|
|
69
65
|
before_model after_model after_model_error
|
|
70
66
|
before_tool after_tool
|
|
71
67
|
].freeze # :nodoc:
|
|
72
|
-
ExecutedTool = Data.define(:result, :
|
|
68
|
+
ExecutedTool = Data.define(:result, :execution_result) do # :nodoc:
|
|
73
69
|
def status = result.status
|
|
74
70
|
def content = result.content
|
|
71
|
+
def value = execution_result.value
|
|
72
|
+
def artifacts = execution_result.artifacts
|
|
73
|
+
def presentation_content = execution_result.presentation_content
|
|
75
74
|
end
|
|
76
75
|
|
|
77
76
|
extend Support::ClassAttributes
|
|
@@ -84,6 +83,7 @@ module LittleGhost
|
|
|
84
83
|
class_attribute :system_prompt_value
|
|
85
84
|
class_attribute :system_prompt_builder_value
|
|
86
85
|
class_attribute :tool_declarations_value, default: []
|
|
86
|
+
class_attribute :code_mode_configuration_value
|
|
87
87
|
class_attribute :prompt_local_values, default: {}
|
|
88
88
|
class_attribute :callback_values, default: Support::Callbacks.new(*CALLBACKS)
|
|
89
89
|
|
|
@@ -254,8 +254,9 @@ module LittleGhost
|
|
|
254
254
|
|
|
255
255
|
# Adds tool or provider classes to the agent.
|
|
256
256
|
#
|
|
257
|
-
# Every declaration must be a class.
|
|
258
|
-
#
|
|
257
|
+
# Every declaration must be a class. Pass Tool classes directly, or pass
|
|
258
|
+
# provider classes that supply tools dynamically through
|
|
259
|
+
# <tt>tools(binding)</tt>. Multiple declarations are cumulative.
|
|
259
260
|
def tools(*values)
|
|
260
261
|
invalid = values.flatten.compact.find { |value| !value.is_a?(Class) }
|
|
261
262
|
if invalid
|
|
@@ -269,6 +270,20 @@ module LittleGhost
|
|
|
269
270
|
|
|
270
271
|
def tool_declarations = tool_declarations_value # :nodoc:
|
|
271
272
|
|
|
273
|
+
# Enables code mode for this agent. +except+ names the application Tools
|
|
274
|
+
# that remain model-facing; every other application Tool moves into the
|
|
275
|
+
# engine catalog and is called through the parent-process Broker.
|
|
276
|
+
# Framework-owned subagent controls remain model-facing automatically.
|
|
277
|
+
def code_mode(engine: nil, except: nil, **options)
|
|
278
|
+
unknown = options.keys - %i[sandbox limits]
|
|
279
|
+
raise ArgumentError, "unknown keyword: #{unknown.first.inspect}" unless unknown.empty?
|
|
280
|
+
|
|
281
|
+
declaration = options.merge(engine:, except:).compact
|
|
282
|
+
self.code_mode_configuration_value = declaration.freeze
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def code_mode_configuration = code_mode_configuration_value # :nodoc:
|
|
286
|
+
|
|
272
287
|
# Adds a named value or resolver to every prompt rendered for the agent.
|
|
273
288
|
def prompt_local(name, *values, &resolver)
|
|
274
289
|
raise ArgumentError, "Provide a prompt local value or block" if values.empty? && !resolver
|
|
@@ -300,8 +315,9 @@ module LittleGhost
|
|
|
300
315
|
##
|
|
301
316
|
# Runs before one invocation begins.
|
|
302
317
|
#
|
|
303
|
-
# The payload
|
|
304
|
-
#
|
|
318
|
+
# The payload is <tt>{messages: Array<Message>}</tt>. A replacement must
|
|
319
|
+
# contain +:messages+. Cancellation stops the invocation. A callback may
|
|
320
|
+
# also accept <tt>context:</tt> to receive the current RunContext.
|
|
305
321
|
#
|
|
306
322
|
# :singleton-method: before_invocation
|
|
307
323
|
# :call-seq:
|
|
@@ -310,6 +326,10 @@ module LittleGhost
|
|
|
310
326
|
##
|
|
311
327
|
# Observes or transforms the terminal invocation payload.
|
|
312
328
|
#
|
|
329
|
+
# The payload is <tt>{result: RunResult}</tt>. A replacement must contain
|
|
330
|
+
# +:result+. Cancellation stops result delivery. A callback may accept
|
|
331
|
+
# <tt>context:</tt>.
|
|
332
|
+
#
|
|
313
333
|
# :singleton-method: after_invocation
|
|
314
334
|
# :call-seq:
|
|
315
335
|
# after_invocation(callable = nil, prepend: false) { |payload| ... } -> self
|
|
@@ -317,6 +337,11 @@ module LittleGhost
|
|
|
317
337
|
##
|
|
318
338
|
# Runs before a model request is sent.
|
|
319
339
|
#
|
|
340
|
+
# The payload contains +:request+ (ModelRequest), zero-based +:turn+, and
|
|
341
|
+
# +:parent_operation_id+. A replacement must contain +:request+.
|
|
342
|
+
# Cancellation stops the invocation. A callback may accept
|
|
343
|
+
# <tt>context:</tt>.
|
|
344
|
+
#
|
|
320
345
|
# :singleton-method: before_model
|
|
321
346
|
# :call-seq:
|
|
322
347
|
# before_model(callable = nil, prepend: false) { |payload| ... } -> self
|
|
@@ -324,6 +349,11 @@ module LittleGhost
|
|
|
324
349
|
##
|
|
325
350
|
# Observes or transforms a successful model response.
|
|
326
351
|
#
|
|
352
|
+
# The payload contains +:request+, +:response+ (ModelResponse), and
|
|
353
|
+
# zero-based +:turn+. A replacement must contain +:response+.
|
|
354
|
+
# Cancellation stops the invocation. A callback may accept
|
|
355
|
+
# <tt>context:</tt>.
|
|
356
|
+
#
|
|
327
357
|
# :singleton-method: after_model
|
|
328
358
|
# :call-seq:
|
|
329
359
|
# after_model(callable = nil, prepend: false) { |payload| ... } -> self
|
|
@@ -331,6 +361,11 @@ module LittleGhost
|
|
|
331
361
|
##
|
|
332
362
|
# Handles a model error before it leaves the agent loop.
|
|
333
363
|
#
|
|
364
|
+
# The payload contains +:request+, +:error+, zero-based +:turn+, and
|
|
365
|
+
# +:parent_operation_id+. Replacing +:request+ with a ModelRequest retries
|
|
366
|
+
# the model call, up to the framework recovery limit. Cancellation stops
|
|
367
|
+
# the invocation. A callback may accept <tt>context:</tt>.
|
|
368
|
+
#
|
|
334
369
|
# :singleton-method: after_model_error
|
|
335
370
|
# :call-seq:
|
|
336
371
|
# after_model_error(callable = nil, prepend: false) { |payload| ... } -> self
|
|
@@ -338,6 +373,11 @@ module LittleGhost
|
|
|
338
373
|
##
|
|
339
374
|
# Runs after validation but before a tool call starts.
|
|
340
375
|
#
|
|
376
|
+
# The payload contains +:tool_use+, the bound +:tool+, +:operation_id+,
|
|
377
|
+
# and +:parent_operation_id+. Cancellation returns a model-visible Tool
|
|
378
|
+
# error, so its reason must be safe to disclose. Replacements are not
|
|
379
|
+
# consumed. A callback may accept <tt>context:</tt>.
|
|
380
|
+
#
|
|
341
381
|
# :singleton-method: before_tool
|
|
342
382
|
# :call-seq:
|
|
343
383
|
# before_tool(callable = nil, prepend: false) { |payload| ... } -> self
|
|
@@ -345,6 +385,10 @@ module LittleGhost
|
|
|
345
385
|
##
|
|
346
386
|
# Observes or transforms a completed tool result.
|
|
347
387
|
#
|
|
388
|
+
# The payload contains the before-tool fields plus the normalized
|
|
389
|
+
# +:result+. A replacement must contain +:result+.
|
|
390
|
+
# Cancellation is not consumed. A callback may accept <tt>context:</tt>.
|
|
391
|
+
#
|
|
348
392
|
# :singleton-method: after_tool
|
|
349
393
|
# :call-seq:
|
|
350
394
|
# after_tool(callable = nil, prepend: false) { |payload| ... } -> self
|
|
@@ -539,6 +583,7 @@ module LittleGhost
|
|
|
539
583
|
self.class.tool_declarations.each do |declaration|
|
|
540
584
|
@tool_registry.register(declaration, replace: true)
|
|
541
585
|
end
|
|
586
|
+
initialize_code_mode
|
|
542
587
|
@structured_output_strategy = StructuredOutput.resolve(
|
|
543
588
|
self.class.result_schema,
|
|
544
589
|
model:,
|
|
@@ -564,9 +609,13 @@ module LittleGhost
|
|
|
564
609
|
raise ArgumentError, "max_turns must be at least 1" if @max_turns < 1
|
|
565
610
|
raise ArgumentError, "max_tool_calls must be at least 1" if @max_tool_calls < 1
|
|
566
611
|
raise ArgumentError, "max_tool_result_tokens must be at least 1" if @max_tool_result_tokens < 1
|
|
612
|
+
@artifact_lifecycle = if runtime&.respond_to?(:runtime_hooks)
|
|
613
|
+
runtime.runtime_hooks.find { |hook| hook.is_a?(Runtime::Hooks::Artifacts) }
|
|
614
|
+
end
|
|
567
615
|
apply_cancellation_decision!(run_callbacks(:after_initialize, self))
|
|
568
616
|
rescue
|
|
569
617
|
@tool_registry&.close
|
|
618
|
+
@code_mode_runtime&.close
|
|
570
619
|
raise
|
|
571
620
|
end
|
|
572
621
|
|
|
@@ -576,7 +625,19 @@ module LittleGhost
|
|
|
576
625
|
def entrypoint_name = self.class.agent_id # :nodoc:
|
|
577
626
|
|
|
578
627
|
def dispatch_tools(tool_uses, context:, events:, parent_operation_id:, parent_trace_context: nil) # :nodoc:
|
|
579
|
-
|
|
628
|
+
counted = tool_uses.count { |tool_use| !code_mode_control_tool?(tool_use) }
|
|
629
|
+
context.record_tool_calls!(counted, maximum: @max_tool_calls) if counted.positive?
|
|
630
|
+
execute_tools(
|
|
631
|
+
tool_uses,
|
|
632
|
+
context,
|
|
633
|
+
events,
|
|
634
|
+
parent_operation_id:,
|
|
635
|
+
parent_trace_context:
|
|
636
|
+
)
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def code_mode_runtime # :nodoc:
|
|
640
|
+
@code_mode_runtime || raise(ConfigurationError, "code mode is not enabled for this agent")
|
|
580
641
|
end
|
|
581
642
|
|
|
582
643
|
def request_assembly_transition(value, context:) # :nodoc:
|
|
@@ -668,9 +729,9 @@ module LittleGhost
|
|
|
668
729
|
# Streams one invocation as StreamEvent objects.
|
|
669
730
|
#
|
|
670
731
|
# Agents built inside a run accept history, JSON-like context, cancellation,
|
|
671
|
-
# deadlines, settings, and trusted invocation template paths. An
|
|
672
|
-
# instance may be streamed only
|
|
673
|
-
#
|
|
732
|
+
# deadlines, settings, and trusted invocation template paths. An Agent
|
|
733
|
+
# instance may be streamed only by its owning Run. Every template path must
|
|
734
|
+
# be an application-created TrustedPath;
|
|
674
735
|
# the wrapper records a trust decision and must never contain unchecked
|
|
675
736
|
# request or model input.
|
|
676
737
|
def stream(
|
|
@@ -747,6 +808,7 @@ module LittleGhost
|
|
|
747
808
|
interjections.close(error)
|
|
748
809
|
raise
|
|
749
810
|
ensure
|
|
811
|
+
@code_mode_runtime&.close(context: run_context)
|
|
750
812
|
interjections.close(AgentInterjectionError.new("Agent finished before the interjection was delivered"))
|
|
751
813
|
unregister_interjections(interjections)
|
|
752
814
|
end
|
|
@@ -765,7 +827,7 @@ module LittleGhost
|
|
|
765
827
|
end.freeze
|
|
766
828
|
end
|
|
767
829
|
|
|
768
|
-
# The
|
|
830
|
+
# The Tool registry available during this Agent run.
|
|
769
831
|
def tools = tool_registry
|
|
770
832
|
|
|
771
833
|
# Closes owned tools, interjections, sandbox, and workspace resources.
|
|
@@ -776,7 +838,7 @@ module LittleGhost
|
|
|
776
838
|
|
|
777
839
|
@closed = true
|
|
778
840
|
[
|
|
779
|
-
[tool_registry, (@sandbox if @owns_resources), (@workspace if @owns_resources)],
|
|
841
|
+
[@code_mode_runtime, tool_registry, (@sandbox if @owns_resources), (@workspace if @owns_resources)],
|
|
780
842
|
@interjections_mutex.synchronize { @active_interjections.dup }
|
|
781
843
|
]
|
|
782
844
|
end
|
|
@@ -805,7 +867,15 @@ module LittleGhost
|
|
|
805
867
|
# Returns the tools exposed to the model for +turn+. Subclasses may override
|
|
806
868
|
# this hook to filter the already-authorized tool list.
|
|
807
869
|
def model_tools(tools, context:, turn:)
|
|
808
|
-
tools
|
|
870
|
+
return tools unless @code_mode_runtime
|
|
871
|
+
|
|
872
|
+
exceptions = Array(@code_mode_declaration[:except]).map(&:to_s)
|
|
873
|
+
tools.select do |specification|
|
|
874
|
+
name = specification.fetch(:name, specification["name"]).to_s
|
|
875
|
+
tool = tool_registry.fetch(name) if tool_registry.names.include?(name)
|
|
876
|
+
exceptions.include?(name) || %w[exec wait stop].include?(name) ||
|
|
877
|
+
tool.is_a?(Subagents::ControlTool) || !tool
|
|
878
|
+
end
|
|
809
879
|
end
|
|
810
880
|
|
|
811
881
|
# :doc:
|
|
@@ -906,6 +976,12 @@ module LittleGhost
|
|
|
906
976
|
)
|
|
907
977
|
started_at = monotonic_time
|
|
908
978
|
operation_id = SecureRandom.uuid
|
|
979
|
+
events = agent_stream_events(
|
|
980
|
+
events,
|
|
981
|
+
input:,
|
|
982
|
+
operation_id:,
|
|
983
|
+
parent_operation_id:
|
|
984
|
+
)
|
|
909
985
|
context.bind_agent_operation_id(operation_id)
|
|
910
986
|
@assembly_transitions_mutex.synchronize { @assembly_transition = nil }
|
|
911
987
|
interjections.bind(operation_id, target_operation_id: parent_operation_id)
|
|
@@ -923,7 +999,6 @@ module LittleGhost
|
|
|
923
999
|
prompt = rendered_system_prompt(template_locals, template_paths)
|
|
924
1000
|
messages.unshift(Message.new(role: :system, content: prompt)) unless prompt.to_s.empty?
|
|
925
1001
|
messages << (input.is_a?(Message) ? input : Message.new(role: :user, content: input))
|
|
926
|
-
tool_call_count = 0
|
|
927
1002
|
structured_result_repair_due = false
|
|
928
1003
|
|
|
929
1004
|
decision = run_callbacks(:before_invocation, {messages: messages}, context: context)
|
|
@@ -951,7 +1026,7 @@ module LittleGhost
|
|
|
951
1026
|
structured_result_repair_due:,
|
|
952
1027
|
interjections:
|
|
953
1028
|
)
|
|
954
|
-
messages.reject! { |message|
|
|
1029
|
+
messages.reject! { |message| artifact_presentation_message?(message) }
|
|
955
1030
|
messages << response.message
|
|
956
1031
|
tool_uses = response.message.content.grep(Content::ToolUse)
|
|
957
1032
|
result_tool_uses = structured_result_tool_uses(tool_uses)
|
|
@@ -1140,9 +1215,6 @@ module LittleGhost
|
|
|
1140
1215
|
)
|
|
1141
1216
|
end
|
|
1142
1217
|
|
|
1143
|
-
tool_call_count += tool_uses.length
|
|
1144
|
-
raise ProtocolError, "The agent reached its maximum tool calls" if tool_call_count > @max_tool_calls
|
|
1145
|
-
|
|
1146
1218
|
executed_tools = with_assembly_tool_batch(context, tool_uses.length) do
|
|
1147
1219
|
dispatch_tools(
|
|
1148
1220
|
tool_uses,
|
|
@@ -1153,15 +1225,16 @@ module LittleGhost
|
|
|
1153
1225
|
end
|
|
1154
1226
|
messages << Message.new(
|
|
1155
1227
|
role: :tool,
|
|
1156
|
-
content: executed_tools.map(&:result)
|
|
1228
|
+
content: executed_tools.map(&:result),
|
|
1229
|
+
metadata: artifact_message_metadata(executed_tools)
|
|
1157
1230
|
)
|
|
1158
1231
|
executed_tools.each do |executed_tool|
|
|
1159
|
-
next if executed_tool.
|
|
1232
|
+
next if executed_tool.presentation_content.empty?
|
|
1160
1233
|
|
|
1161
1234
|
messages << Message.new(
|
|
1162
1235
|
role: :user,
|
|
1163
|
-
content: executed_tool.
|
|
1164
|
-
metadata: {transient: true,
|
|
1236
|
+
content: executed_tool.presentation_content,
|
|
1237
|
+
metadata: {transient: true, little_ghost_artifact_presentation: true}
|
|
1165
1238
|
)
|
|
1166
1239
|
end
|
|
1167
1240
|
context.checkpoint(messages)
|
|
@@ -1328,6 +1401,7 @@ module LittleGhost
|
|
|
1328
1401
|
response = nil
|
|
1329
1402
|
time_to_first_token = nil
|
|
1330
1403
|
buffered_events = strategy ? [] : nil
|
|
1404
|
+
code_mode_control_indexes = {}
|
|
1331
1405
|
|
|
1332
1406
|
model.stream(request).each do |event|
|
|
1333
1407
|
context.check!
|
|
@@ -1348,7 +1422,9 @@ module LittleGhost
|
|
|
1348
1422
|
**model_attributes
|
|
1349
1423
|
)
|
|
1350
1424
|
end
|
|
1351
|
-
|
|
1425
|
+
unless code_mode_control_event?(event, code_mode_control_indexes)
|
|
1426
|
+
buffered_events ? buffered_events << event : events << event
|
|
1427
|
+
end
|
|
1352
1428
|
response = event.data[:response] if event.type == :message_stop
|
|
1353
1429
|
end
|
|
1354
1430
|
raise ProtocolError, "The model stream ended without a response" unless response
|
|
@@ -1471,7 +1547,8 @@ module LittleGhost
|
|
|
1471
1547
|
raise ProtocolError, "The model returned duplicate tool use ids"
|
|
1472
1548
|
end
|
|
1473
1549
|
|
|
1474
|
-
tool_uses.each { |tool_use| emit(events, :tool_start, tool_use: tool_use) }
|
|
1550
|
+
tool_uses.each { |tool_use| emit(events, :tool_start, tool_use: tool_use) unless code_mode_control_tool?(tool_use) }
|
|
1551
|
+
presentation_budget = Artifacts::PresentationBudget.new
|
|
1475
1552
|
pairs = tool_uses.map do |tool_use|
|
|
1476
1553
|
[tool_use, tool_registry.fetch(tool_use.name)]
|
|
1477
1554
|
rescue ToolError => error
|
|
@@ -1510,7 +1587,8 @@ module LittleGhost
|
|
|
1510
1587
|
exception: diagnostic_tool_exception(tool, tool:)
|
|
1511
1588
|
}
|
|
1512
1589
|
)
|
|
1513
|
-
|
|
1590
|
+
execution_result = Tool::ExecutionResult.new(content: result.content, status: :error, error: tool)
|
|
1591
|
+
next ExecutedTool.new(result:, execution_result:)
|
|
1514
1592
|
end
|
|
1515
1593
|
|
|
1516
1594
|
context.check!
|
|
@@ -1542,7 +1620,8 @@ module LittleGhost
|
|
|
1542
1620
|
exception: diagnostic_tool_exception(rejection, tool:)
|
|
1543
1621
|
}
|
|
1544
1622
|
)
|
|
1545
|
-
|
|
1623
|
+
execution_result = Tool::ExecutionResult.new(content: result.content, status: :error, error: rejection)
|
|
1624
|
+
next ExecutedTool.new(result:, execution_result:)
|
|
1546
1625
|
end
|
|
1547
1626
|
|
|
1548
1627
|
execution_context = ToolExecution.new(
|
|
@@ -1555,20 +1634,36 @@ module LittleGhost
|
|
|
1555
1634
|
parent_trace_context:
|
|
1556
1635
|
)
|
|
1557
1636
|
invoke = lambda do
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1637
|
+
ExecutionState.with(tool_execution: execution_context) do
|
|
1638
|
+
with_tool_execution(execution_context) do
|
|
1639
|
+
invoke_tool(
|
|
1640
|
+
tool_use, tool, context,
|
|
1641
|
+
operation_id:, parent_operation_id:
|
|
1642
|
+
)
|
|
1643
|
+
end
|
|
1563
1644
|
end
|
|
1564
1645
|
end
|
|
1565
|
-
tool_result = tool.exclusive? ?
|
|
1646
|
+
tool_result = if tool.exclusive? && !code_mode_control_tool?(tool_use)
|
|
1647
|
+
synchronize_exclusive_tools(&invoke)
|
|
1648
|
+
else
|
|
1649
|
+
invoke.call
|
|
1650
|
+
end
|
|
1566
1651
|
after_decision = run_callbacks(
|
|
1567
1652
|
:after_tool,
|
|
1568
1653
|
callback_payload.merge(result: tool_result),
|
|
1569
1654
|
context: context
|
|
1570
1655
|
)
|
|
1571
1656
|
tool_result = replacement_value(after_decision, :result, tool_result)
|
|
1657
|
+
if @artifact_lifecycle
|
|
1658
|
+
tool_result = @artifact_lifecycle.prepare_tool_result(
|
|
1659
|
+
tool_result,
|
|
1660
|
+
tool_use:,
|
|
1661
|
+
run: @run,
|
|
1662
|
+
workspace:,
|
|
1663
|
+
context:
|
|
1664
|
+
)
|
|
1665
|
+
end
|
|
1666
|
+
tool_result = bound_artifact_presentation(tool_result, presentation_budget)
|
|
1572
1667
|
result = build_tool_result(
|
|
1573
1668
|
tool_use_id: tool_use.id,
|
|
1574
1669
|
content: tool_result.content,
|
|
@@ -1589,7 +1684,7 @@ module LittleGhost
|
|
|
1589
1684
|
exception: tool_error && diagnostic_tool_exception(tool_error, tool:)
|
|
1590
1685
|
}.compact
|
|
1591
1686
|
)
|
|
1592
|
-
ExecutedTool.new(result:,
|
|
1687
|
+
ExecutedTool.new(result:, execution_result: tool_result)
|
|
1593
1688
|
rescue ToolError => error
|
|
1594
1689
|
result = build_tool_result(tool_use_id: tool_use.id, content: error.message, status: :error)
|
|
1595
1690
|
finish_instrumentation(
|
|
@@ -1605,7 +1700,8 @@ module LittleGhost
|
|
|
1605
1700
|
exception: diagnostic_tool_exception(error, tool:)
|
|
1606
1701
|
}
|
|
1607
1702
|
)
|
|
1608
|
-
|
|
1703
|
+
execution_result = Tool::ExecutionResult.new(content: result.content, status: :error, error:)
|
|
1704
|
+
ExecutedTool.new(result:, execution_result:)
|
|
1609
1705
|
rescue => error
|
|
1610
1706
|
finish_instrumentation(
|
|
1611
1707
|
tool_handle,
|
|
@@ -1622,7 +1718,7 @@ module LittleGhost
|
|
|
1622
1718
|
if tools.any?(&:exclusive?)
|
|
1623
1719
|
pairs.map do |tool_use, tool|
|
|
1624
1720
|
executed_tool = execution.call(tool_use, tool)
|
|
1625
|
-
emit(events, :tool_stop, tool_use:, result: executed_tool.result)
|
|
1721
|
+
emit(events, :tool_stop, tool_use:, result: executed_tool.result) unless code_mode_control_tool?(tool_use)
|
|
1626
1722
|
executed_tool
|
|
1627
1723
|
end
|
|
1628
1724
|
else
|
|
@@ -1630,7 +1726,8 @@ module LittleGhost
|
|
|
1630
1726
|
pairs,
|
|
1631
1727
|
cancellation_token: context.cancellation_token,
|
|
1632
1728
|
on_result: lambda do |index, executed_tool|
|
|
1633
|
-
|
|
1729
|
+
tool_use = tool_uses.fetch(index)
|
|
1730
|
+
emit(events, :tool_stop, tool_use:, result: executed_tool.result) unless code_mode_control_tool?(tool_use)
|
|
1634
1731
|
end
|
|
1635
1732
|
) do |tool_use, tool|
|
|
1636
1733
|
execution.call(tool_use, tool)
|
|
@@ -1638,10 +1735,82 @@ module LittleGhost
|
|
|
1638
1735
|
end
|
|
1639
1736
|
end
|
|
1640
1737
|
|
|
1738
|
+
def bound_artifact_presentation(result, budget)
|
|
1739
|
+
accepted = budget.accept(result.presentation_content)
|
|
1740
|
+
return result if accepted.equal?(result.presentation_content) || accepted == result.presentation_content
|
|
1741
|
+
|
|
1742
|
+
fallback = result.artifacts.reject do |artifact|
|
|
1743
|
+
artifact.metadata[:complete_result] || artifact.metadata["complete_result"]
|
|
1744
|
+
end
|
|
1745
|
+
references = artifact_references(fallback)
|
|
1746
|
+
content = if references.empty?
|
|
1747
|
+
result.content
|
|
1748
|
+
else
|
|
1749
|
+
"#{result.content}\n\nArtifacts:\n#{references.join("\n")}"
|
|
1750
|
+
end
|
|
1751
|
+
|
|
1752
|
+
Tool::ExecutionResult.new(
|
|
1753
|
+
value: result.value,
|
|
1754
|
+
content:,
|
|
1755
|
+
status: result.status,
|
|
1756
|
+
error: result.error,
|
|
1757
|
+
artifacts: result.artifacts,
|
|
1758
|
+
presentation_content: accepted
|
|
1759
|
+
)
|
|
1760
|
+
end
|
|
1761
|
+
|
|
1762
|
+
def artifact_references(artifacts)
|
|
1763
|
+
artifacts.filter_map do |artifact|
|
|
1764
|
+
next unless artifact.reference.is_a?(String) && artifact.bytes
|
|
1765
|
+
|
|
1766
|
+
details = [artifact.media_type, "#{artifact.bytes} bytes"].compact.join(", ")
|
|
1767
|
+
"- #{artifact.reference} (#{details})"
|
|
1768
|
+
end
|
|
1769
|
+
end
|
|
1770
|
+
|
|
1771
|
+
def artifact_message_metadata(executed_tools)
|
|
1772
|
+
artifacts = executed_tools.flat_map(&:artifacts).select do |artifact|
|
|
1773
|
+
artifact.reference.is_a?(String) && artifact.bytes
|
|
1774
|
+
end
|
|
1775
|
+
return {} if artifacts.empty?
|
|
1776
|
+
|
|
1777
|
+
{
|
|
1778
|
+
"little_ghost_artifacts" => artifacts.map do |artifact|
|
|
1779
|
+
{
|
|
1780
|
+
"reference" => artifact.reference,
|
|
1781
|
+
"name" => artifact.name,
|
|
1782
|
+
"media_type" => artifact.media_type,
|
|
1783
|
+
"bytes" => artifact.bytes
|
|
1784
|
+
}.compact
|
|
1785
|
+
end
|
|
1786
|
+
}
|
|
1787
|
+
end
|
|
1788
|
+
|
|
1641
1789
|
def invoke_tool(tool_use, tool, context, operation_id:, parent_operation_id:)
|
|
1642
1790
|
tool.execute(tool_use.input, context:)
|
|
1643
1791
|
end
|
|
1644
1792
|
|
|
1793
|
+
def code_mode_control_tool?(tool_use)
|
|
1794
|
+
@code_mode_runtime && %w[exec wait stop].include?(tool_use.name)
|
|
1795
|
+
end
|
|
1796
|
+
|
|
1797
|
+
def code_mode_control_event?(event, indexes)
|
|
1798
|
+
return false unless @code_mode_runtime
|
|
1799
|
+
|
|
1800
|
+
index = event.data[:index]
|
|
1801
|
+
case event.type
|
|
1802
|
+
when :tool_call_start
|
|
1803
|
+
indexes[index] = true if %w[exec wait stop].include?(event.data[:name])
|
|
1804
|
+
indexes.key?(index)
|
|
1805
|
+
when :tool_call_delta
|
|
1806
|
+
indexes.key?(index)
|
|
1807
|
+
when :tool_call_stop
|
|
1808
|
+
indexes.delete(index)
|
|
1809
|
+
else
|
|
1810
|
+
false
|
|
1811
|
+
end
|
|
1812
|
+
end
|
|
1813
|
+
|
|
1645
1814
|
def parent_trace_attributes(trace_context)
|
|
1646
1815
|
return {} unless trace_context.is_a?(Hash) && !trace_context.empty?
|
|
1647
1816
|
|
|
@@ -1938,19 +2107,39 @@ module LittleGhost
|
|
|
1938
2107
|
|
|
1939
2108
|
def rendered_system_prompt(locals, invocation_paths)
|
|
1940
2109
|
prompt = self.class.system_prompt
|
|
1941
|
-
|
|
1942
|
-
return prompt if prompt
|
|
2110
|
+
prompt = prompt.call(locals) if prompt.respond_to?(:call)
|
|
2111
|
+
return append_code_mode_instructions(prompt) if prompt
|
|
1943
2112
|
template = self.class.system_template
|
|
1944
|
-
return DEFAULT_SYSTEM_PROMPT if instance_of?(Agent) && run && !template
|
|
2113
|
+
return append_code_mode_instructions(DEFAULT_SYSTEM_PROMPT) if instance_of?(Agent) && run && !template
|
|
1945
2114
|
|
|
1946
2115
|
template ||= "#{self.class.logical_path}/system" if run
|
|
1947
2116
|
return nil unless template
|
|
1948
2117
|
|
|
1949
|
-
@template_resolver.render(
|
|
2118
|
+
append_code_mode_instructions(@template_resolver.render(
|
|
1950
2119
|
template,
|
|
1951
2120
|
locals: locals,
|
|
1952
2121
|
invocation_paths: invocation_paths
|
|
1953
|
-
)
|
|
2122
|
+
))
|
|
2123
|
+
end
|
|
2124
|
+
|
|
2125
|
+
def initialize_code_mode
|
|
2126
|
+
declaration = self.class.code_mode_configuration
|
|
2127
|
+
return unless declaration
|
|
2128
|
+
|
|
2129
|
+
defaults = @runtime.respond_to?(:code_mode_configuration) ? @runtime.code_mode_configuration : nil
|
|
2130
|
+
@code_mode_declaration = (defaults || {}).merge(declaration).transform_keys(&:to_sym).freeze
|
|
2131
|
+
unknown = @code_mode_declaration.keys - %i[engine except sandbox limits]
|
|
2132
|
+
raise ConfigurationError, "Unknown code-mode option: #{unknown.first.inspect}" unless unknown.empty?
|
|
2133
|
+
@code_mode_runtime = CodeMode::AgentRuntime.new(agent: self, declaration: @code_mode_declaration)
|
|
2134
|
+
@tool_registry.register(CodeMode::ExecTool)
|
|
2135
|
+
@tool_registry.register(CodeMode::WaitTool)
|
|
2136
|
+
@tool_registry.register(CodeMode::StopTool)
|
|
2137
|
+
end
|
|
2138
|
+
|
|
2139
|
+
def append_code_mode_instructions(prompt)
|
|
2140
|
+
return prompt unless @code_mode_runtime
|
|
2141
|
+
|
|
2142
|
+
[prompt, @code_mode_runtime.instructions].compact.reject(&:empty?).join("\n\n")
|
|
1954
2143
|
end
|
|
1955
2144
|
|
|
1956
2145
|
def default_template_resolver(paths)
|
|
@@ -1976,6 +2165,19 @@ module LittleGhost
|
|
|
1976
2165
|
events << StreamEvent.build(type, **data)
|
|
1977
2166
|
end
|
|
1978
2167
|
|
|
2168
|
+
def agent_stream_events(events, input:, operation_id:, parent_operation_id:)
|
|
2169
|
+
return events unless run&.include_agent_events?
|
|
2170
|
+
|
|
2171
|
+
source = AgentStreamSource.build(
|
|
2172
|
+
agent_id: self.class.agent_id,
|
|
2173
|
+
agent_path:,
|
|
2174
|
+
operation_id:,
|
|
2175
|
+
parent_operation_id:,
|
|
2176
|
+
assembly_path: agent_stream_path
|
|
2177
|
+
)
|
|
2178
|
+
AgentStreamSink.new(destination: events, run:, source:, input:)
|
|
2179
|
+
end
|
|
2180
|
+
|
|
1979
2181
|
def instrument(name, **attributes)
|
|
1980
2182
|
attributes.delete(:diagnostic) unless self.class.capture_diagnostics
|
|
1981
2183
|
values = correlation_attributes.merge(attributes.compact)
|
|
@@ -2086,8 +2288,8 @@ module LittleGhost
|
|
|
2086
2288
|
value.respond_to?(:role) ? diagnostic_message(value) : value.to_s
|
|
2087
2289
|
end
|
|
2088
2290
|
|
|
2089
|
-
def
|
|
2090
|
-
message.metadata[:
|
|
2291
|
+
def artifact_presentation_message?(message)
|
|
2292
|
+
message.metadata[:little_ghost_artifact_presentation] || message.metadata["little_ghost_artifact_presentation"]
|
|
2091
2293
|
end
|
|
2092
2294
|
|
|
2093
2295
|
def diagnostic_tool_exception(error, tool:)
|