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.
- checksums.yaml +4 -4
- data/README.md +90 -84
- data/docs/guides/assemblies.md +412 -0
- data/docs/guides/code_mode.md +275 -0
- data/docs/guides/core_concepts.md +150 -239
- data/docs/guides/getting_started.md +125 -87
- data/docs/guides/integrations.md +217 -0
- data/docs/guides/models_and_providers.md +125 -0
- data/docs/guides/production.md +253 -0
- data/docs/guides/prompt_views.md +139 -0
- 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 +5 -5
- data/lib/little_ghost/agent/delegation.rb +3 -3
- 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 +411 -214
- data/lib/little_ghost/agent_builder.rb +61 -22
- data/lib/little_ghost/{agent_interruptions.rb → agent_interjections.rb} +12 -12
- 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 +110 -30
- data/lib/little_ghost/assembly_builder.rb +59 -21
- data/lib/little_ghost/assembly_execution.rb +110 -6
- 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 +366 -47
- data/lib/little_ghost/content.rb +24 -13
- data/lib/little_ghost/data_map.rb +209 -0
- data/lib/little_ghost/errors.rb +28 -9
- data/lib/little_ghost/execution.rb +33 -34
- data/lib/little_ghost/graph.rb +376 -197
- 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/message.rb +4 -4
- 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 +15 -5
- 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 +9 -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 +162 -54
- data/lib/little_ghost/run_context.rb +42 -20
- data/lib/little_ghost/run_result.rb +0 -7
- data/lib/little_ghost/runtime/hook.rb +8 -3
- data/lib/little_ghost/runtime/hooks/artifacts.rb +338 -0
- data/lib/little_ghost/runtime.rb +160 -52
- 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 +39 -26
- data/lib/little_ghost/session_store.rb +9 -5
- data/lib/little_ghost/session_stores/agent_core_memory.rb +64 -56
- data/lib/little_ghost/session_stores/filesystem.rb +261 -0
- data/lib/little_ghost/session_stores/memory.rb +7 -0
- data/lib/little_ghost/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 +53 -50
- 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/swarm.rb +13 -5
- data/lib/little_ghost/tool.rb +157 -64
- 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/tools/write_todos.rb +6 -1
- data/lib/little_ghost/tracing/open_telemetry.rb +1 -1
- data/lib/little_ghost/version.rb +1 -1
- data/lib/little_ghost/workflow.rb +30 -21
- data/lib/little_ghost/workspace.rb +222 -8
- data/lib/little_ghost.rb +40 -27
- metadata +104 -3
- 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
|
|
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
|
|
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
|
|
42
|
+
# Creates an event with a symbol +type+ and frozen outer payload Hash.
|
|
48
43
|
end
|
|
49
44
|
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,
|
|
24
|
-
#
|
|
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
|
|
28
|
-
# #
|
|
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 =
|
|
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
|
-
|
|
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
|
-
:
|
|
69
|
-
:
|
|
70
|
-
:
|
|
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
|
|
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 =
|
|
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?(:
|
|
384
|
-
raise ToolError, "Subagent #{subagent_id.inspect} does not support
|
|
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.
|
|
390
|
-
raise ToolError, "Subagent #{subagent_id.inspect} has reached its
|
|
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
|
-
|
|
393
|
-
if
|
|
394
|
-
raise ToolError, "Subagent
|
|
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.
|
|
400
|
+
value.current.interjections << exchange
|
|
398
401
|
[value, value.current]
|
|
399
402
|
end
|
|
400
403
|
|
|
401
|
-
|
|
402
|
-
identity.agent.
|
|
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.
|
|
413
|
+
turn.interjections.delete(exchange)
|
|
411
414
|
@condition.broadcast
|
|
412
415
|
end
|
|
413
416
|
raise
|
|
414
417
|
end
|
|
415
|
-
response =
|
|
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.
|
|
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: "
|
|
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:
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
563
|
-
finalization; use
|
|
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
|
-
|
|
589
|
-
name: "
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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[:
|
|
1161
|
-
options[:
|
|
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
|
-
|
|
1288
|
-
while identity.durable && turn.
|
|
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.
|
|
1300
|
+
turn.interjections.select(&:complete).map { |exchange| [exchange.message, exchange.response] }
|
|
1298
1301
|
end
|
|
1299
|
-
return unless
|
|
1302
|
+
return unless interject_exchanges
|
|
1300
1303
|
|
|
1301
|
-
retain_agent_conversation(identity, turn, result, persisted_response,
|
|
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,
|
|
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
|
-
|
|
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)
|
|
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
|
-
# ===
|
|
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
|
|
23
|
-
# policy per
|
|
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
|
-
# ===
|
|
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
|
|
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
|
|
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 = [
|
data/lib/little_ghost/swarm.rb
CHANGED
|
@@ -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
|
|
11
|
-
# to
|
|
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
|
-
#
|
|
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
|
|
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
|
|
112
|
+
# Validates the members and allowed handoff routes, then returns this class.
|
|
105
113
|
def validate!
|
|
106
114
|
swarm_definition!
|
|
107
115
|
self
|