little_ghost 0.3.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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +90 -84
  3. data/docs/guides/assemblies.md +412 -0
  4. data/docs/guides/code_mode.md +275 -0
  5. data/docs/guides/core_concepts.md +150 -239
  6. data/docs/guides/getting_started.md +125 -87
  7. data/docs/guides/integrations.md +217 -0
  8. data/docs/guides/models_and_providers.md +125 -0
  9. data/docs/guides/production.md +253 -0
  10. data/docs/guides/prompt_views.md +139 -0
  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 +5 -5
  16. data/lib/little_ghost/agent/delegation.rb +3 -3
  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 +411 -214
  20. data/lib/little_ghost/agent_builder.rb +61 -22
  21. data/lib/little_ghost/{agent_interruptions.rb → agent_interjections.rb} +12 -12
  22. data/lib/little_ghost/agent_stream_source.rb +262 -0
  23. data/lib/little_ghost/artifact.rb +182 -0
  24. data/lib/little_ghost/artifacts/presentation_budget.rb +44 -0
  25. data/lib/little_ghost/artifacts/workspace_store.rb +370 -0
  26. data/lib/little_ghost/artifacts.rb +11 -0
  27. data/lib/little_ghost/assembly.rb +110 -30
  28. data/lib/little_ghost/assembly_builder.rb +59 -21
  29. data/lib/little_ghost/assembly_execution.rb +110 -6
  30. data/lib/little_ghost/code_mode/broker.rb +164 -0
  31. data/lib/little_ghost/code_mode/catalog.rb +54 -0
  32. data/lib/little_ghost/code_mode/engine.rb +58 -0
  33. data/lib/little_ghost/code_mode/javascript/catalog.rb +118 -0
  34. data/lib/little_ghost/code_mode/javascript/client.rb +550 -0
  35. data/lib/little_ghost/code_mode/javascript/host.rb +347 -0
  36. data/lib/little_ghost/code_mode/javascript/session.rb +579 -0
  37. data/lib/little_ghost/code_mode/javascript_engine.rb +172 -0
  38. data/lib/little_ghost/code_mode/protocol.rb +78 -0
  39. data/lib/little_ghost/code_mode/ruby/catalog.rb +80 -0
  40. data/lib/little_ghost/code_mode/ruby/host.rb +102 -0
  41. data/lib/little_ghost/code_mode/ruby/session.rb +508 -0
  42. data/lib/little_ghost/code_mode/ruby_engine.rb +86 -0
  43. data/lib/little_ghost/code_mode/runtime.rb +335 -0
  44. data/lib/little_ghost/code_mode/session.rb +61 -0
  45. data/lib/little_ghost/code_mode/types.rb +58 -0
  46. data/lib/little_ghost/code_mode.rb +53 -0
  47. data/lib/little_ghost/configuration.rb +366 -47
  48. data/lib/little_ghost/content.rb +24 -13
  49. data/lib/little_ghost/data_map.rb +209 -0
  50. data/lib/little_ghost/errors.rb +28 -9
  51. data/lib/little_ghost/execution.rb +33 -34
  52. data/lib/little_ghost/graph.rb +376 -197
  53. data/lib/little_ghost/mcp/client.rb +487 -88
  54. data/lib/little_ghost/mcp/toolset.rb +210 -0
  55. data/lib/little_ghost/mcp/types.rb +216 -0
  56. data/lib/little_ghost/mcp.rb +3 -0
  57. data/lib/little_ghost/message.rb +4 -4
  58. data/lib/little_ghost/model_capabilities.rb +9 -6
  59. data/lib/little_ghost/model_request.rb +0 -12
  60. data/lib/little_ghost/model_resolver.rb +15 -5
  61. data/lib/little_ghost/model_response.rb +3 -7
  62. data/lib/little_ghost/network/authorizer_server.rb +162 -0
  63. data/lib/little_ghost/network/certificate_authority.rb +98 -0
  64. data/lib/little_ghost/network/envoy_config.rb +362 -0
  65. data/lib/little_ghost/network/envoy_gateway.rb +409 -0
  66. data/lib/little_ghost/network/external_gateway.rb +68 -0
  67. data/lib/little_ghost/network.rb +96 -0
  68. data/lib/little_ghost/prompt_resolver.rb +9 -7
  69. data/lib/little_ghost/provider_registry.rb +3 -3
  70. data/lib/little_ghost/providers/anthropic.rb +8 -1
  71. data/lib/little_ghost/providers/gemini.rb +10 -1
  72. data/lib/little_ghost/providers/vertex_ai.rb +6 -1
  73. data/lib/little_ghost/run.rb +162 -54
  74. data/lib/little_ghost/run_context.rb +42 -20
  75. data/lib/little_ghost/run_result.rb +0 -7
  76. data/lib/little_ghost/runtime/hook.rb +8 -3
  77. data/lib/little_ghost/runtime/hooks/artifacts.rb +338 -0
  78. data/lib/little_ghost/runtime.rb +160 -52
  79. data/lib/little_ghost/sandbox/capabilities.rb +68 -0
  80. data/lib/little_ghost/sandbox/environment_policy.rb +41 -0
  81. data/lib/little_ghost/sandbox/filesystem.rb +377 -0
  82. data/lib/little_ghost/sandbox/isolated_backend.rb +163 -0
  83. data/lib/little_ghost/sandbox/limits.rb +48 -0
  84. data/lib/little_ghost/sandbox/mount.rb +129 -0
  85. data/lib/little_ghost/sandbox/network_policy.rb +108 -0
  86. data/lib/little_ghost/sandbox/policy.rb +88 -0
  87. data/lib/little_ghost/sandbox/process_runner.rb +103 -0
  88. data/lib/little_ghost/sandbox/process_session.rb +335 -0
  89. data/lib/little_ghost/sandbox/scope.rb +304 -0
  90. data/lib/little_ghost/sandbox.rb +158 -32
  91. data/lib/little_ghost/sandboxes/bubblewrap.rb +351 -0
  92. data/lib/little_ghost/sandboxes/native.rb +51 -0
  93. data/lib/little_ghost/sandboxes/seatbelt.rb +261 -0
  94. data/lib/little_ghost/sandboxes/unrestricted.rb +241 -0
  95. data/lib/little_ghost/session.rb +39 -26
  96. data/lib/little_ghost/session_store.rb +9 -5
  97. data/lib/little_ghost/session_stores/agent_core_memory.rb +64 -56
  98. data/lib/little_ghost/session_stores/filesystem.rb +261 -0
  99. data/lib/little_ghost/session_stores/memory.rb +7 -0
  100. data/lib/little_ghost/skills/catalog.rb +94 -14
  101. data/lib/little_ghost/skills/resource_root.rb +42 -0
  102. data/lib/little_ghost/skills/skill.rb +0 -3
  103. data/lib/little_ghost/stream_event.rb +8 -13
  104. data/lib/little_ghost/subagents/control_tool.rb +8 -0
  105. data/lib/little_ghost/subagents/manager.rb +53 -50
  106. data/lib/little_ghost/support/callbacks.rb +3 -1
  107. data/lib/little_ghost/support/content_capture.rb +3 -3
  108. data/lib/little_ghost/support/http_client.rb +2 -2
  109. data/lib/little_ghost/support/redactor.rb +1 -1
  110. data/lib/little_ghost/swarm.rb +13 -5
  111. data/lib/little_ghost/tool.rb +157 -64
  112. data/lib/little_ghost/tool_registry.rb +1 -1
  113. data/lib/little_ghost/tools/filesystem.rb +1 -1
  114. data/lib/little_ghost/tools/shell.rb +4 -3
  115. data/lib/little_ghost/tools/write_todos.rb +6 -1
  116. data/lib/little_ghost/tracing/open_telemetry.rb +1 -1
  117. data/lib/little_ghost/version.rb +1 -1
  118. data/lib/little_ghost/workflow.rb +30 -21
  119. data/lib/little_ghost/workspace.rb +222 -8
  120. data/lib/little_ghost.rb +40 -27
  121. metadata +104 -3
  122. data/lib/little_ghost/unrestricted_sandbox.rb +0 -306
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module LittleGhost
6
+ module Skills
7
+ module ResourceRoot # :nodoc:
8
+ module_function
9
+
10
+ def normalize(value)
11
+ return unless value
12
+
13
+ path = value.to_s
14
+ return normalize_workspace_reference(path) if path.start_with?("workspace://")
15
+
16
+ unless valid_absolute_path?(path)
17
+ raise ArgumentError, "resource_root must be an absolute path or workspace:// reference"
18
+ end
19
+
20
+ File.expand_path(path).freeze
21
+ end
22
+
23
+ def normalize_workspace_reference(path)
24
+ logical = path.delete_prefix("workspace://")
25
+ components = logical.split("/", -1)
26
+ name = components.shift
27
+ invalid = name.nil? || !name.match?(/\A[a-zA-Z0-9_-]+\z/) ||
28
+ components.any? { |component| component.empty? || component == "." || component == ".." }
29
+ if invalid || path.include?("\0") || path.include?("\\")
30
+ raise ArgumentError, "resource_root must be an absolute path or workspace:// reference"
31
+ end
32
+
33
+ path.dup.freeze
34
+ end
35
+
36
+ def valid_absolute_path?(path)
37
+ !path.include?("\0") && Pathname.new(path).absolute? &&
38
+ !path.split(File::SEPARATOR).include?("..")
39
+ end
40
+ end
41
+ end
42
+ end
@@ -3,9 +3,6 @@
3
3
  module LittleGhost
4
4
  # Skills give agents focused instructions and supporting resources on demand.
5
5
  module Skills
6
- # Skill holds the metadata and instructions loaded from one +SKILL.md+ file.
7
- # +path+ is shown to the model; +source_path+ is the local file used for
8
- # boundary validation and resource discovery.
9
6
  Skill = Data.define( # :nodoc:
10
7
  :name,
11
8
  :description,
@@ -1,17 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LittleGhost
4
- # StreamEvent gives every provider and interface the same language for live
5
- # agent output. Consumers can handle text, reasoning, tools, usage, retries,
6
- # and completion without branching on a provider SDK.
7
- #
8
- # Providers emit events such as +:message_start+, +:text_delta+,
9
- # +:reasoning_delta+, +:tool_call_start+, +:tool_call_delta+,
10
- # +:tool_call_stop+, +:usage+, +:model_retry+, and +:message_stop+. The
11
- # terminal event carries a {ModelResponse}[rdoc-ref:LittleGhost::ModelResponse]
12
- # in +data[:response]+.
13
4
  StreamEvent = Data.define(:type, :data) do # :nodoc:
14
- # Creates an immutable event with a symbol +type+ and keyword payload.
5
+ # Creates an event with a symbol +type+ and frozen outer payload Hash.
15
6
  def self.build(type, **data)
16
7
  new(type: type.to_sym, data: data.freeze)
17
8
  end
@@ -25,7 +16,10 @@ module LittleGhost
25
16
  # +:reasoning_delta+, +:tool_call_start+, +:tool_call_delta+,
26
17
  # +:tool_call_stop+, +:usage+, +:model_retry+, and +:message_stop+. The
27
18
  # terminal event carries a {ModelResponse}[rdoc-ref:LittleGhost::ModelResponse]
28
- # in +data[:response]+.
19
+ # in +data[:response]+. An +:agent_stream+ event wraps a copied, frozen Agent
20
+ # event with an
21
+ # AgentStreamSource[rdoc-ref:LittleGhost::AgentStreamSource] when a Run exposes
22
+ # nested work.
29
23
  #
30
24
  # event = LittleGhost::StreamEvent.build(:text_delta, text: "Hello")
31
25
  # event.type # => :text_delta
@@ -37,13 +31,14 @@ module LittleGhost
37
31
 
38
32
  ##
39
33
  # :attr_reader: data
40
- # The immutable payload for this event kind.
34
+ # The frozen outer payload Hash. Nested values are retained and must not be
35
+ # mutated by callers.
41
36
 
42
37
  ##
43
38
  # :singleton-method: build
44
39
  # :call-seq:
45
40
  # build(type, **data) -> StreamEvent
46
41
  #
47
- # Creates an immutable event with a symbol +type+ and keyword payload.
42
+ # Creates an event with a symbol +type+ and frozen outer payload Hash.
48
43
  end
49
44
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LittleGhost
4
+ module Subagents
5
+ class ControlTool < Tool # :nodoc:
6
+ end
7
+ end
8
+ end
@@ -5,6 +5,7 @@ require "base64"
5
5
  require "digest"
6
6
  require "time"
7
7
  require_relative "definition"
8
+ require_relative "control_tool"
8
9
 
9
10
  module LittleGhost
10
11
  module Subagents
@@ -20,12 +21,14 @@ module LittleGhost
20
21
  # description: "Investigates policies and account history"
21
22
  # end
22
23
  #
23
- # LittleGhost then gives +CustomerSupportAgent+ tools to spawn, message, wait for,
24
- # interrupt, and list research agents. The manager keeps each child identity
25
- # stable across follow-up turns.
24
+ # LittleGhost then gives +CustomerSupportAgent+ tools to spawn, message,
25
+ # check on, interject, and list research agents. The manager keeps each child
26
+ # identity stable across follow-up turns. A progress check returns after 30
27
+ # seconds by default when the selected subagents are still working; it does
28
+ # not pause or restart them.
26
29
  #
27
- # Follow-up messages are FIFO turns and never interrupt active work.
28
- # #interrupt is the separate synchronous path for delivery at the next model
30
+ # Follow-up messages are FIFO turns and never interject active work.
31
+ # #interject is the separate synchronous path for delivery at the next model
29
32
  # boundary; delivery does not stop the child, and tool calls from that model
30
33
  # response continue in the child run.
31
34
  #
@@ -46,7 +49,7 @@ module LittleGhost
46
49
  DEFAULT_MAX_QUEUED_TURNS_PER_IDENTITY = 8 # :nodoc:
47
50
  DEFAULT_MAX_MESSAGE_CHARS = 50_000 # :nodoc:
48
51
  DEFAULT_MAX_RESPONSE_CHARS = 100_000 # :nodoc:
49
- DEFAULT_WAIT_TIMEOUT = 20.0 # :nodoc:
52
+ DEFAULT_WAIT_TIMEOUT = 30.0 # :nodoc:
50
53
  DEFAULT_CLOSE_TIMEOUT = 5.0 # :nodoc:
51
54
  DEFAULT_LIST_LIMIT = 20 # :nodoc:
52
55
  MAX_LIST_LIMIT = 100 # :nodoc:
@@ -58,16 +61,16 @@ module LittleGhost
58
61
  CURSOR_MAX_BYTES = 512 # :nodoc:
59
62
  UUID_PATTERN = /\A[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\z/ # :nodoc:
60
63
 
61
- InterruptExchange = Struct.new(:message, :response, :complete) # :nodoc:
64
+ InterjectionExchange = Struct.new(:message, :response, :complete) # :nodoc:
62
65
  Turn = Struct.new( # :nodoc:
63
66
  :number,
64
67
  :message,
65
68
  :completion,
66
69
  :operation_id,
67
70
  :parent_operation_id,
68
- :interrupts,
69
- :interruption_metadata,
70
- :interruption_ids
71
+ :interjections,
72
+ :interjection_metadata,
73
+ :interjection_ids
71
74
  )
72
75
  Identity = Struct.new( # :nodoc:
73
76
  :subagent_id,
@@ -368,7 +371,7 @@ module LittleGhost
368
371
  # model response. The returned +response_disposition+ says whether that
369
372
  # response also initiated tool calls; it does not imply the subagent has
370
373
  # stopped.
371
- def interrupt(subagent_id:, message:, cancellation_token: @cancellation_token, deadline: @deadline)
374
+ def interject(subagent_id:, message:, cancellation_token: @cancellation_token, deadline: @deadline)
372
375
  unless message.is_a?(String)
373
376
  raise ToolError, "Subagent messages must be strings."
374
377
  end
@@ -376,30 +379,30 @@ module LittleGhost
376
379
  raise ToolError, "Subagent messages cannot exceed #{@max_message_chars} characters."
377
380
  end
378
381
 
379
- exchange = InterruptExchange.new(message:, complete: false)
382
+ exchange = InterjectionExchange.new(message:, complete: false)
380
383
  identity, turn = @mutex.synchronize do
381
384
  ensure_open!
382
385
  value = fetch_identity!(subagent_id)
383
- unless value.agent.respond_to?(:interrupt_response)
384
- raise ToolError, "Subagent #{subagent_id.inspect} does not support interruptions."
386
+ unless value.agent.respond_to?(:interject)
387
+ raise ToolError, "Subagent #{subagent_id.inspect} does not support interjections."
385
388
  end
386
389
  unless value.status == "running"
387
390
  raise ToolError, "Subagent #{subagent_id.inspect} is not currently running."
388
391
  end
389
- if value.current.interrupts.length >= @max_queued_turns_per_identity
390
- raise ToolError, "Subagent #{subagent_id.inspect} has reached its interrupt limit."
392
+ if value.current.interjections.length >= @max_queued_turns_per_identity
393
+ raise ToolError, "Subagent #{subagent_id.inspect} has reached its interject limit."
391
394
  end
392
- interrupt_chars = value.current.interrupts.sum { |pending| pending.message.length }
393
- if interrupt_chars + message.length > @max_message_chars
394
- raise ToolError, "Subagent interrupt messages cannot exceed #{@max_message_chars} total characters."
395
+ interjection_chars = value.current.interjections.sum { |pending| pending.message.length }
396
+ if interjection_chars + message.length > @max_message_chars
397
+ raise ToolError, "Subagent interjection messages cannot exceed #{@max_message_chars} total characters."
395
398
  end
396
399
 
397
- value.current.interrupts << exchange
400
+ value.current.interjections << exchange
398
401
  [value, value.current]
399
402
  end
400
403
 
401
- interrupt_response = begin
402
- identity.agent.interrupt_response(
404
+ result = begin
405
+ identity.agent.interject(
403
406
  message,
404
407
  cancellation_token:,
405
408
  deadline:,
@@ -407,16 +410,16 @@ module LittleGhost
407
410
  )
408
411
  rescue
409
412
  @mutex.synchronize do
410
- turn.interrupts.delete(exchange)
413
+ turn.interjections.delete(exchange)
411
414
  @condition.broadcast
412
415
  end
413
416
  raise
414
417
  end
415
- response = interrupt_response.text
418
+ response = result.text
416
419
  truncated = response.length > @max_response_chars
417
420
  returned_response = truncated ? response[0, @max_response_chars] : response
418
421
  @mutex.synchronize do
419
- used_response_chars = turn.interrupts.sum do |pending|
422
+ used_response_chars = turn.interjections.sum do |pending|
420
423
  pending.equal?(exchange) ? 0 : pending.response.to_s.length
421
424
  end
422
425
  remaining_response_chars = [@max_response_chars - used_response_chars, 0].max
@@ -428,17 +431,17 @@ module LittleGhost
428
431
  snapshot(identity, include_response: true, include_progress: true)
429
432
  end
430
433
  value = {
431
- status: "interruption_delivered",
434
+ status: "interjection_delivered",
432
435
  subagent_id: identity.subagent_id,
433
436
  kind: identity.definition.kind,
434
437
  subagent:,
435
438
  turn: turn.number,
436
439
  response: returned_response,
437
- response_disposition: interrupt_response.tool_calls? ? "text_with_tool_calls" : "text_only"
440
+ response_disposition: result.tool_calls? ? "text_with_tool_calls" : "text_only"
438
441
  }
439
442
  value[:response_truncated] = true if truncated
440
443
  value
441
- rescue AgentInterruptError => error
444
+ rescue AgentInterjectionError => error
442
445
  raise ToolError, error.message
443
446
  end
444
447
 
@@ -504,7 +507,7 @@ module LittleGhost
504
507
  end
505
508
  end
506
509
 
507
- # Builds spawn, follow-up, interrupt, wait, and list tools bound to this
510
+ # Builds spawn, follow-up, interject, wait, and list tools bound to this
508
511
  # manager. Closing the first tool closes the shared manager.
509
512
  def tools
510
513
  manager = self
@@ -512,7 +515,7 @@ module LittleGhost
512
515
  "- #{definition.kind}: #{definition.description}"
513
516
  end.join("\n")
514
517
  tools = [
515
- Tool.define(
518
+ ControlTool.define(
516
519
  name: "spawn_subagent",
517
520
  description: <<~DESCRIPTION.strip,
518
521
  Create a new subagent identity for an independent task. Mode controls delivery: sync waits for the
@@ -554,13 +557,13 @@ module LittleGhost
554
557
  parent_operation_id: context&.agent_operation_id
555
558
  )
556
559
  end,
557
- Tool.define(
560
+ ControlTool.define(
558
561
  name: "send_message_to_subagent",
559
562
  description: <<~DESCRIPTION.strip,
560
563
  Send a follow-up turn to an existing active or persisted subagent identity. Persisted conversations
561
564
  are restored transparently before the follow-up. Messages are processed in order after the
562
- current turn and never interrupt active work. Do not use this for status, steering, stopping, or
563
- finalization; use interrupt_subagent for an active subagent. Mode controls delivery: sync waits for the
565
+ current turn and never interject active work. Do not use this for status, steering, stopping, or
566
+ finalization; use interject_subagent for an active subagent. Mode controls delivery: sync waits for the
564
567
  later turn's response, while async enqueues the turn and returns immediately.
565
568
  DESCRIPTION
566
569
  input_schema: {
@@ -585,8 +588,8 @@ module LittleGhost
585
588
  parent_operation_id: context&.agent_operation_id
586
589
  )
587
590
  end,
588
- Tool.define(
589
- name: "interrupt_subagent",
591
+ ControlTool.define(
592
+ name: "interject_subagent",
590
593
  description: <<~DESCRIPTION.strip,
591
594
  Interrupt an actively running subagent in its current turn. The message is added at the next model
592
595
  boundary. This call waits for that model response and reports its ordinary text, whether the same
@@ -606,13 +609,13 @@ module LittleGhost
606
609
  options = {}
607
610
  options[:cancellation_token] = context.cancellation_token if context
608
611
  options[:deadline] = context.deadline if context&.deadline
609
- manager.interrupt(
612
+ manager.interject(
610
613
  subagent_id: input.fetch("subagent_id"),
611
614
  message: input.fetch("message"),
612
615
  **options
613
616
  )
614
617
  end,
615
- Tool.define(
618
+ ControlTool.define(
616
619
  name: "wait_for_subagents",
617
620
  description: <<~DESCRIPTION.strip,
618
621
  Wait briefly for selected subagents, or all subagents when omitted. A still_working response is expected
@@ -633,7 +636,7 @@ module LittleGhost
633
636
  additionalProperties: false
634
637
  }
635
638
  ) { |input| manager.wait(subagent_ids: input["subagent_ids"]) },
636
- Tool.define(
639
+ ControlTool.define(
637
640
  name: "list_subagents",
638
641
  description: <<~DESCRIPTION.strip,
639
642
  List active and persisted subagent conversations newest-first without restoring inactive agents.
@@ -1089,9 +1092,9 @@ module LittleGhost
1089
1092
  completion: Completion.new,
1090
1093
  operation_id: SecureRandom.uuid,
1091
1094
  parent_operation_id:,
1092
- interrupts: [],
1093
- interruption_metadata: context&.interruption_metadata,
1094
- interruption_ids: context&.interruption_ids || []
1095
+ interjections: [],
1096
+ interjection_metadata: context&.interjection_metadata,
1097
+ interjection_ids: context&.interjection_ids || []
1095
1098
  )
1096
1099
  identity.next_turn += 1
1097
1100
  @turn_count += 1 if count_turn
@@ -1157,8 +1160,8 @@ module LittleGhost
1157
1160
  options[:history] = identity.history
1158
1161
  options[:context] = identity.state
1159
1162
  options[:conversation_id] = identity.conversation_id
1160
- options[:interruption_metadata] = turn.interruption_metadata
1161
- options[:interruption_ids] = turn.interruption_ids
1163
+ options[:interjection_metadata] = turn.interjection_metadata
1164
+ options[:interjection_ids] = turn.interjection_ids
1162
1165
  end
1163
1166
  result = if identity.agent.is_a?(Agent)
1164
1167
  run_agent_turn(identity, turn, options)
@@ -1284,8 +1287,8 @@ module LittleGhost
1284
1287
  response = serialized_response[0, @max_response_chars] if truncated
1285
1288
  persisted_response = response.is_a?(String) ? response : serialized_response
1286
1289
 
1287
- interrupt_exchanges = @mutex.synchronize do
1288
- while identity.durable && turn.interrupts.any? { |exchange| !exchange.complete } && !@closed
1290
+ interject_exchanges = @mutex.synchronize do
1291
+ while identity.durable && turn.interjections.any? { |exchange| !exchange.complete } && !@closed
1289
1292
  @condition.wait(@mutex, CANCELLATION_POLL_INTERVAL)
1290
1293
  end
1291
1294
  if @closed
@@ -1294,11 +1297,11 @@ module LittleGhost
1294
1297
  end
1295
1298
 
1296
1299
  identity.status = "persisting" if identity.durable
1297
- turn.interrupts.select(&:complete).map { |exchange| [exchange.message, exchange.response] }
1300
+ turn.interjections.select(&:complete).map { |exchange| [exchange.message, exchange.response] }
1298
1301
  end
1299
- return unless interrupt_exchanges
1302
+ return unless interject_exchanges
1300
1303
 
1301
- retain_agent_conversation(identity, turn, result, persisted_response, interrupt_exchanges)
1304
+ retain_agent_conversation(identity, turn, result, persisted_response, interject_exchanges)
1302
1305
 
1303
1306
  @mutex.synchronize do
1304
1307
  identity.latest_turn = turn.number
@@ -1323,11 +1326,11 @@ module LittleGhost
1323
1326
  end
1324
1327
  end
1325
1328
 
1326
- def retain_agent_conversation(identity, turn, result, persisted_response, interrupt_exchanges)
1329
+ def retain_agent_conversation(identity, turn, result, persisted_response, interject_exchanges)
1327
1330
  if identity.durable
1328
1331
  state = result.is_a?(RunResult) ? result.state : identity.state
1329
1332
  messages = [Message.new(role: :user, content: turn.message)]
1330
- interrupt_exchanges.each do |message, response|
1333
+ interject_exchanges.each do |message, response|
1331
1334
  messages << Message.new(role: :user, content: message)
1332
1335
  messages << Message.new(role: :assistant, content: response)
1333
1336
  end
@@ -16,7 +16,9 @@ module LittleGhost
16
16
  # concrete class.
17
17
  #
18
18
  # callbacks = LittleGhost::Support::Callbacks.new(:prepare)
19
- # callbacks.on(:prepare) { |payload| Callbacks.replace(payload.merge(debug: true)) }
19
+ # callbacks.on(:prepare) do |payload|
20
+ # LittleGhost::Support::Callbacks.replace(payload.merge(debug: true))
21
+ # end
20
22
  # decision = callbacks.run(:prepare, {})
21
23
  # decision.value # => {debug: true}
22
24
  class Callbacks
@@ -15,12 +15,12 @@ module LittleGhost
15
15
  # )
16
16
  # LittleGhost::Instrumentation.capture_content(policy)
17
17
  #
18
- # === Security and trust
18
+ # === Choose captured data
19
19
  #
20
20
  # Enabling capture may place model input, output, tool definitions, and
21
21
  # exception details into telemetry. Redaction and a custom scrubber reduce
22
- # accidental disclosure but are not a security boundary. Configure one
23
- # policy per trusted process and apply exporter-side controls as well.
22
+ # accidental disclosure but cannot recognize every sensitive value. Configure
23
+ # one capture policy per process and apply exporter-side controls as well.
24
24
  class ContentCapture
25
25
  CaptureLimitExceeded = Class.new(StandardError) # :nodoc:
26
26
 
@@ -10,10 +10,10 @@ module LittleGhost
10
10
  # It applies cancellation, deadlines, timeouts, and response-size limits while
11
11
  # yielding response chunks as they arrive.
12
12
  #
13
- # === Security and trust
13
+ # === HTTPS defaults
14
14
  #
15
15
  # HTTPS is required by default. Enabling +allow_insecure_http+ can expose API
16
- # keys and model content in transit; use it only with a trusted local
16
+ # keys and model content in transit; use it only with a local
17
17
  # development endpoint.
18
18
  class HTTPClient
19
19
  # Default upper bound for a complete provider response (50 MiB).
@@ -8,7 +8,7 @@ module LittleGhost
8
8
  #
9
9
  # Redaction is a defense-in-depth aid, not proof that arbitrary
10
10
  # sensitive content is safe to export. Applications should add known secret
11
- # values and keep telemetry within an appropriate trust boundary.
11
+ # values and send telemetry only to the intended destination.
12
12
  class Redactor
13
13
  SENSITIVE_KEY = /(authorization|api[_-]?key|credential|password|secret|(?:^|[_-])token(?:$|[_-])|cookie|private[_-]?key)/i # :nodoc:
14
14
  SECRET_PATTERNS = [
@@ -7,8 +7,8 @@ module LittleGhost
7
7
  # Lets configured Agent members hand one request directly to one another.
8
8
  #
9
9
  # A swarm is an Assembly for model-selected routing. One member is active at a
10
- # time. It either produces the final answer or calls a reserved handoff tool
11
- # to select an allowed next member.
10
+ # time. It either produces the final answer or uses a model-visible handoff
11
+ # tool to choose one of the next members allowed by the application.
12
12
  #
13
13
  # class ProblemSolverSwarm < LittleGhost::Swarm
14
14
  # member TriageAgent
@@ -23,10 +23,15 @@ module LittleGhost
23
23
  # run = ProblemSolverSwarm.ask("Why was I charged twice?")
24
24
  # run.response
25
25
  #
26
+ # Call a named Swarm with
27
+ # ask[rdoc-ref:LittleGhost::Assembly.ask] for its final Run, or
28
+ # the streaming entrypoint[rdoc-ref:LittleGhost::Assembly.stream_ask] for
29
+ # coordination and final-response events.
30
+ #
26
31
  # Swarm members are Agent definitions rather than arbitrary assemblies so a
27
32
  # handoff remains a direct model-to-model transition. Original conversation
28
33
  # history and application context stay isolated unless a member opts in with
29
- # +history: true+ or +context: true+. Streams expose coordination lifecycle
34
+ # <tt>history: true</tt> or <tt>context: true</tt>. Streams expose coordination
30
35
  # events and the final member response, but not intermediate model text.
31
36
  class Swarm < Assembly
32
37
  MAX_BUFFERED_EVENTS = 10_000 # :nodoc:
@@ -91,7 +96,10 @@ module LittleGhost
91
96
  self.swarm_max_steps_value = value
92
97
  end
93
98
 
94
- # Reads or assigns the repeated directed-handoff limit.
99
+ # Reads or assigns how often the same directed handoff may repeat.
100
+ #
101
+ # For example, a value of +2+ allows the transition from triage to billing
102
+ # twice during one Swarm run. +max_steps+ still limits total member calls.
95
103
  def max_handoff_repeats(value = nil)
96
104
  return swarm_max_handoff_repeats_value if value.nil?
97
105
 
@@ -101,7 +109,7 @@ module LittleGhost
101
109
  self.swarm_max_handoff_repeats_value = value
102
110
  end
103
111
 
104
- # Validates the member set and topology and returns this Swarm class.
112
+ # Validates the members and allowed handoff routes, then returns this class.
105
113
  def validate!
106
114
  swarm_definition!
107
115
  self