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.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +28 -6
  3. data/docs/guides/assemblies.md +143 -17
  4. data/docs/guides/code_mode.md +275 -0
  5. data/docs/guides/core_concepts.md +46 -30
  6. data/docs/guides/getting_started.md +17 -6
  7. data/docs/guides/integrations.md +217 -0
  8. data/docs/guides/models_and_providers.md +125 -0
  9. data/docs/guides/production.md +81 -15
  10. data/docs/guides/prompt_views.md +14 -7
  11. data/docs/guides/sandboxing.md +282 -0
  12. data/docs/guides/skills.md +141 -0
  13. data/docs/guides/structured_outputs_and_content.md +135 -0
  14. data/docs/guides/tools.md +329 -0
  15. data/lib/little_ghost/ag_ui/adapter.rb +2 -2
  16. data/lib/little_ghost/agent/delegation.rb +2 -2
  17. data/lib/little_ghost/agent/skills.rb +6 -1
  18. data/lib/little_ghost/agent/tool_loop.rb +6 -2
  19. data/lib/little_ghost/agent.rb +248 -46
  20. data/lib/little_ghost/agent_builder.rb +61 -22
  21. data/lib/little_ghost/agent_stream_source.rb +262 -0
  22. data/lib/little_ghost/artifact.rb +182 -0
  23. data/lib/little_ghost/artifacts/presentation_budget.rb +44 -0
  24. data/lib/little_ghost/artifacts/workspace_store.rb +370 -0
  25. data/lib/little_ghost/artifacts.rb +11 -0
  26. data/lib/little_ghost/assembly.rb +63 -17
  27. data/lib/little_ghost/assembly_builder.rb +26 -26
  28. data/lib/little_ghost/assembly_execution.rb +23 -2
  29. data/lib/little_ghost/code_mode/broker.rb +164 -0
  30. data/lib/little_ghost/code_mode/catalog.rb +54 -0
  31. data/lib/little_ghost/code_mode/engine.rb +58 -0
  32. data/lib/little_ghost/code_mode/javascript/catalog.rb +118 -0
  33. data/lib/little_ghost/code_mode/javascript/client.rb +550 -0
  34. data/lib/little_ghost/code_mode/javascript/host.rb +347 -0
  35. data/lib/little_ghost/code_mode/javascript/session.rb +579 -0
  36. data/lib/little_ghost/code_mode/javascript_engine.rb +172 -0
  37. data/lib/little_ghost/code_mode/protocol.rb +78 -0
  38. data/lib/little_ghost/code_mode/ruby/catalog.rb +80 -0
  39. data/lib/little_ghost/code_mode/ruby/host.rb +102 -0
  40. data/lib/little_ghost/code_mode/ruby/session.rb +508 -0
  41. data/lib/little_ghost/code_mode/ruby_engine.rb +86 -0
  42. data/lib/little_ghost/code_mode/runtime.rb +335 -0
  43. data/lib/little_ghost/code_mode/session.rb +61 -0
  44. data/lib/little_ghost/code_mode/types.rb +58 -0
  45. data/lib/little_ghost/code_mode.rb +53 -0
  46. data/lib/little_ghost/configuration.rb +107 -12
  47. data/lib/little_ghost/content.rb +19 -8
  48. data/lib/little_ghost/data_map.rb +2 -2
  49. data/lib/little_ghost/errors.rb +26 -7
  50. data/lib/little_ghost/execution.rb +1 -2
  51. data/lib/little_ghost/graph.rb +367 -207
  52. data/lib/little_ghost/mcp/client.rb +487 -88
  53. data/lib/little_ghost/mcp/toolset.rb +210 -0
  54. data/lib/little_ghost/mcp/types.rb +216 -0
  55. data/lib/little_ghost/mcp.rb +3 -0
  56. data/lib/little_ghost/model_capabilities.rb +9 -6
  57. data/lib/little_ghost/model_request.rb +0 -12
  58. data/lib/little_ghost/model_resolver.rb +13 -3
  59. data/lib/little_ghost/model_response.rb +3 -7
  60. data/lib/little_ghost/network/authorizer_server.rb +162 -0
  61. data/lib/little_ghost/network/certificate_authority.rb +98 -0
  62. data/lib/little_ghost/network/envoy_config.rb +362 -0
  63. data/lib/little_ghost/network/envoy_gateway.rb +409 -0
  64. data/lib/little_ghost/network/external_gateway.rb +68 -0
  65. data/lib/little_ghost/network.rb +96 -0
  66. data/lib/little_ghost/prompt_resolver.rb +7 -7
  67. data/lib/little_ghost/provider_registry.rb +3 -3
  68. data/lib/little_ghost/providers/anthropic.rb +8 -1
  69. data/lib/little_ghost/providers/gemini.rb +10 -1
  70. data/lib/little_ghost/providers/vertex_ai.rb +6 -1
  71. data/lib/little_ghost/run.rb +83 -13
  72. data/lib/little_ghost/run_context.rb +9 -0
  73. data/lib/little_ghost/run_result.rb +0 -7
  74. data/lib/little_ghost/runtime/hook.rb +5 -0
  75. data/lib/little_ghost/runtime/hooks/artifacts.rb +338 -0
  76. data/lib/little_ghost/runtime.rb +102 -34
  77. data/lib/little_ghost/sandbox/capabilities.rb +68 -0
  78. data/lib/little_ghost/sandbox/environment_policy.rb +41 -0
  79. data/lib/little_ghost/sandbox/filesystem.rb +377 -0
  80. data/lib/little_ghost/sandbox/isolated_backend.rb +163 -0
  81. data/lib/little_ghost/sandbox/limits.rb +48 -0
  82. data/lib/little_ghost/sandbox/mount.rb +129 -0
  83. data/lib/little_ghost/sandbox/network_policy.rb +108 -0
  84. data/lib/little_ghost/sandbox/policy.rb +88 -0
  85. data/lib/little_ghost/sandbox/process_runner.rb +103 -0
  86. data/lib/little_ghost/sandbox/process_session.rb +335 -0
  87. data/lib/little_ghost/sandbox/scope.rb +304 -0
  88. data/lib/little_ghost/sandbox.rb +158 -32
  89. data/lib/little_ghost/sandboxes/bubblewrap.rb +351 -0
  90. data/lib/little_ghost/sandboxes/native.rb +51 -0
  91. data/lib/little_ghost/sandboxes/seatbelt.rb +261 -0
  92. data/lib/little_ghost/sandboxes/unrestricted.rb +241 -0
  93. data/lib/little_ghost/session.rb +30 -6
  94. data/lib/little_ghost/skills/catalog.rb +94 -14
  95. data/lib/little_ghost/skills/resource_root.rb +42 -0
  96. data/lib/little_ghost/skills/skill.rb +0 -3
  97. data/lib/little_ghost/stream_event.rb +8 -13
  98. data/lib/little_ghost/subagents/control_tool.rb +8 -0
  99. data/lib/little_ghost/subagents/manager.rb +12 -9
  100. data/lib/little_ghost/support/callbacks.rb +3 -1
  101. data/lib/little_ghost/support/content_capture.rb +3 -3
  102. data/lib/little_ghost/support/http_client.rb +2 -2
  103. data/lib/little_ghost/support/redactor.rb +1 -1
  104. data/lib/little_ghost/tool.rb +104 -53
  105. data/lib/little_ghost/tool_registry.rb +1 -1
  106. data/lib/little_ghost/tools/filesystem.rb +1 -1
  107. data/lib/little_ghost/tools/shell.rb +4 -3
  108. data/lib/little_ghost/version.rb +1 -1
  109. data/lib/little_ghost/workspace.rb +222 -8
  110. data/lib/little_ghost.rb +21 -12
  111. metadata +98 -2
  112. data/lib/little_ghost/unrestricted_sandbox.rb +0 -306
@@ -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, :companion_content) do # :nodoc:
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. A provider class can supply tools
258
- # dynamically by implementing <tt>tools(binding)</tt>.
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 may be continued, replaced, or cancelled with a decision
304
- # from Support::Callbacks.
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
- execute_tools(tool_uses, context, events, parent_operation_id:, parent_trace_context:)
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 agent
672
- # instance may be streamed only according to the lifecycle managed by its
673
- # owning run. Every template path must be an application-created TrustedPath;
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 materialized tools available during this agent run.
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| tool_companion_message?(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.companion_content.empty?
1232
+ next if executed_tool.presentation_content.empty?
1160
1233
 
1161
1234
  messages << Message.new(
1162
1235
  role: :user,
1163
- content: executed_tool.companion_content,
1164
- metadata: {transient: true, little_ghost_tool_companion: 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
- buffered_events ? buffered_events << event : events << event
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
- next ExecutedTool.new(result:, companion_content: [])
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
- next ExecutedTool.new(result:, companion_content: [])
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
- with_tool_execution(execution_context) do
1559
- invoke_tool(
1560
- tool_use, tool, context,
1561
- operation_id:, parent_operation_id:
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? ? synchronize_exclusive_tools(&invoke) : invoke.call
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:, companion_content: tool_result.companion_content)
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
- ExecutedTool.new(result:, companion_content: [])
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
- emit(events, :tool_stop, tool_use: tool_uses.fetch(index), result: executed_tool.result)
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
- return prompt.call(locals) if prompt.respond_to?(:call)
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 tool_companion_message?(message)
2090
- message.metadata[:little_ghost_tool_companion] || message.metadata["little_ghost_tool_companion"]
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:)