little_ghost 0.5.0 → 0.7.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 +3 -3
- data/docs/guides/code_mode.md +6 -3
- data/docs/guides/integrations.md +4 -4
- data/docs/guides/models_and_providers.md +91 -0
- data/docs/guides/production.md +114 -7
- data/docs/guides/tools.md +26 -0
- data/lib/little_ghost/agent.rb +5 -5
- data/lib/little_ghost/assembly.rb +9 -11
- data/lib/little_ghost/assembly_execution.rb +8 -17
- data/lib/little_ghost/code_mode/broker.rb +4 -0
- data/lib/little_ghost/code_mode/javascript/session.rb +23 -24
- data/lib/little_ghost/code_mode/ruby/host.rb +50 -10
- data/lib/little_ghost/code_mode/ruby/session.rb +22 -18
- data/lib/little_ghost/code_mode/ruby_engine.rb +1 -0
- data/lib/little_ghost/configuration.rb +101 -9
- data/lib/little_ghost/embeddings/request.rb +103 -0
- data/lib/little_ghost/embeddings/response.rb +55 -0
- data/lib/little_ghost/errors.rb +2 -0
- data/lib/little_ghost/execution.rb +31 -25
- data/lib/little_ghost/execution_state.rb +3 -3
- data/lib/little_ghost/graph.rb +6 -3
- data/lib/little_ghost/instrumentation.rb +2 -1
- data/lib/little_ghost/mcp/toolset.rb +1 -1
- data/lib/little_ghost/model.rb +20 -0
- data/lib/little_ghost/model_operations.rb +210 -0
- data/lib/little_ghost/network/certificate_authority.rb +6 -0
- data/lib/little_ghost/network/envoy_gateway.rb +11 -2
- data/lib/little_ghost/providers/base.rb +16 -1
- data/lib/little_ghost/providers/bedrock/aws_protocol.rb +1 -1
- data/lib/little_ghost/providers/bedrock/http_client.rb +28 -7
- data/lib/little_ghost/providers/bedrock.rb +93 -8
- data/lib/little_ghost/providers/openai.rb +80 -4
- data/lib/little_ghost/run.rb +9 -15
- data/lib/little_ghost/run_context.rb +2 -1
- data/lib/little_ghost/runtime/hooks/artifacts.rb +8 -0
- data/lib/little_ghost/runtime.rb +35 -3
- data/lib/little_ghost/sandbox/process_runner.rb +1 -1
- data/lib/little_ghost/sandboxes/unrestricted.rb +1 -2
- data/lib/little_ghost/session_stores/filesystem.rb +29 -13
- data/lib/little_ghost/subagents/manager.rb +170 -70
- data/lib/little_ghost/support/cancellation_token.rb +2 -2
- data/lib/little_ghost/support/executor.rb +84 -15
- data/lib/little_ghost/support/http_client.rb +19 -5
- data/lib/little_ghost/support/interruptible_stream.rb +4 -2
- data/lib/little_ghost/support/pooled_thread_runner.rb +126 -0
- data/lib/little_ghost/support/serialized_dispatcher.rb +84 -0
- data/lib/little_ghost/support/task.rb +126 -0
- data/lib/little_ghost/support/task_runner.rb +74 -0
- data/lib/little_ghost/support.rb +4 -0
- data/lib/little_ghost/tracing/open_telemetry.rb +6 -4
- data/lib/little_ghost/version.rb +1 -1
- data/lib/little_ghost/workflow.rb +3 -3
- data/lib/little_ghost.rb +42 -0
- metadata +27 -6
|
@@ -6,6 +6,9 @@ module LittleGhost
|
|
|
6
6
|
module Host # :nodoc:
|
|
7
7
|
SOURCE = <<~'RUBY'
|
|
8
8
|
require "json"
|
|
9
|
+
protocol_output = STDOUT.dup
|
|
10
|
+
protocol_output.sync = true
|
|
11
|
+
STDOUT.reopen(STDERR)
|
|
9
12
|
STDOUT.sync = true
|
|
10
13
|
begin
|
|
11
14
|
read_exactly = lambda do |length|
|
|
@@ -21,9 +24,9 @@ module LittleGhost
|
|
|
21
24
|
write_frame = lambda do |value|
|
|
22
25
|
payload = JSON.generate(value)
|
|
23
26
|
raise "protocol frame too large" if payload.bytesize > 64 * 1024 * 1024
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
protocol_output.write([payload.bytesize].pack("N"))
|
|
28
|
+
protocol_output.write(payload)
|
|
29
|
+
protocol_output.flush
|
|
27
30
|
end
|
|
28
31
|
request = read_frame.call
|
|
29
32
|
catalog = request.fetch("catalog")
|
|
@@ -32,7 +35,40 @@ module LittleGhost
|
|
|
32
35
|
response_queues = {}
|
|
33
36
|
calls = 0
|
|
34
37
|
max_calls = request.fetch("tool_calls")
|
|
35
|
-
|
|
38
|
+
output_buffer = +""
|
|
39
|
+
flush_output = lambda do
|
|
40
|
+
unless output_buffer.empty?
|
|
41
|
+
value = output_buffer.dup
|
|
42
|
+
output_buffer.clear
|
|
43
|
+
write_frame.call(type: "text", value: value)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
emit = lambda do |value|
|
|
47
|
+
write_lock.synchronize do
|
|
48
|
+
flush_output.call
|
|
49
|
+
write_frame.call(value)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
program_output = Object.new
|
|
53
|
+
program_output.define_singleton_method(:write) do |value|
|
|
54
|
+
value = String(value)
|
|
55
|
+
write_lock.synchronize do
|
|
56
|
+
output_buffer << value
|
|
57
|
+
flush_output.call if output_buffer.bytesize >= 16_384 || value.include?("\n")
|
|
58
|
+
end
|
|
59
|
+
value.bytesize
|
|
60
|
+
end
|
|
61
|
+
program_output.define_singleton_method(:flush) do
|
|
62
|
+
write_lock.synchronize { flush_output.call }
|
|
63
|
+
self
|
|
64
|
+
end
|
|
65
|
+
program_output.define_singleton_method(:sync) { false }
|
|
66
|
+
program_output.define_singleton_method(:sync=) do |value|
|
|
67
|
+
flush if value
|
|
68
|
+
value
|
|
69
|
+
end
|
|
70
|
+
program_output.define_singleton_method(:tty?) { false }
|
|
71
|
+
$stdout = program_output
|
|
36
72
|
reader = Thread.new do
|
|
37
73
|
loop do
|
|
38
74
|
response = read_frame.call
|
|
@@ -51,9 +87,7 @@ module LittleGhost
|
|
|
51
87
|
response_queues[id] = queue
|
|
52
88
|
[id, queue]
|
|
53
89
|
end
|
|
54
|
-
|
|
55
|
-
write_frame.call(type: "call", id: id, name: name, arguments: arguments)
|
|
56
|
-
end
|
|
90
|
+
emit.call(type: "call", id: id, name: name, arguments: arguments)
|
|
57
91
|
response = queue.pop
|
|
58
92
|
queues_lock.synchronize { response_queues.delete(id) }
|
|
59
93
|
raise response.fetch("error") if response["error"]
|
|
@@ -75,7 +109,12 @@ module LittleGhost
|
|
|
75
109
|
end
|
|
76
110
|
Object.const_set(:ALL_TOOLS, catalog.freeze) unless Object.const_defined?(:ALL_TOOLS)
|
|
77
111
|
Object.const_set(:FRAME, request["frame"].freeze) if request["frame"] && !Object.const_defined?(:FRAME)
|
|
78
|
-
|
|
112
|
+
evaluation_context = Class.new do
|
|
113
|
+
def evaluate(source)
|
|
114
|
+
instance_eval(source, "(code-mode)", 1)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
context = evaluation_context.new
|
|
79
118
|
finished = false
|
|
80
119
|
finish_value = nil
|
|
81
120
|
context.define_singleton_method(:tools) { tools }
|
|
@@ -85,14 +124,15 @@ module LittleGhost
|
|
|
85
124
|
finish_value = value
|
|
86
125
|
throw :little_ghost_finish
|
|
87
126
|
end
|
|
88
|
-
value = catch(:little_ghost_finish) { context.
|
|
127
|
+
value = catch(:little_ghost_finish) { context.evaluate(request.fetch("source")) }
|
|
89
128
|
value = finish_value if finished
|
|
90
129
|
emit.call(type: "done", value: value)
|
|
91
130
|
rescue SignalException
|
|
92
131
|
exit 0
|
|
93
132
|
rescue Exception => error
|
|
94
133
|
STDERR.puts("#{error.class}: #{error.message}")
|
|
95
|
-
|
|
134
|
+
error_frame = {type: "error", error: "#{error.class}: #{error.message}"}
|
|
135
|
+
emit ? emit.call(error_frame) : write_frame&.call(error_frame)
|
|
96
136
|
exit 1
|
|
97
137
|
end
|
|
98
138
|
RUBY
|
|
@@ -14,6 +14,7 @@ module LittleGhost
|
|
|
14
14
|
|
|
15
15
|
def initialize(broker:, sandbox_factory:, subprocess_policy:, limits:, observation_seconds: OBSERVATION_SECONDS)
|
|
16
16
|
@broker = broker
|
|
17
|
+
@task_runner = broker.task_runner
|
|
17
18
|
@sandbox_factory = sandbox_factory
|
|
18
19
|
@subprocess_policy = subprocess_policy
|
|
19
20
|
@limits = limits
|
|
@@ -28,8 +29,9 @@ module LittleGhost
|
|
|
28
29
|
@control_mutex = Mutex.new
|
|
29
30
|
@programs = 0
|
|
30
31
|
@call_mutex = Mutex.new
|
|
31
|
-
@
|
|
32
|
+
@call_tasks = []
|
|
32
33
|
@call_errors = []
|
|
34
|
+
@tool_calls = 0
|
|
33
35
|
@closing_marker = {closing: false}
|
|
34
36
|
@lifecycle_mutex = Mutex.new
|
|
35
37
|
@lifecycle_condition = ConditionVariable.new
|
|
@@ -190,6 +192,7 @@ module LittleGhost
|
|
|
190
192
|
@output = +""
|
|
191
193
|
@output_bytes = 0
|
|
192
194
|
@buffer = +""
|
|
195
|
+
@call_mutex.synchronize { @tool_calls = 0 }
|
|
193
196
|
@lifecycle_mutex.synchronize { @generation = generation }
|
|
194
197
|
start_watchdog(generation, @deadline)
|
|
195
198
|
[generation, @session]
|
|
@@ -268,20 +271,20 @@ module LittleGhost
|
|
|
268
271
|
end
|
|
269
272
|
|
|
270
273
|
def dispatch_call(message, process)
|
|
271
|
-
|
|
272
|
-
active = @
|
|
274
|
+
@call_mutex.synchronize do
|
|
275
|
+
active = @call_tasks.count(&:alive?)
|
|
273
276
|
raise ProtocolError, "code-mode concurrent tool call limit exceeded" if active >= @limits.fetch(:concurrency)
|
|
277
|
+
@tool_calls += 1
|
|
278
|
+
raise ToolError, "tool call limit exceeded" if @tool_calls > @limits.fetch(:tool_calls)
|
|
274
279
|
|
|
275
280
|
call_errors = @call_errors
|
|
276
281
|
closing_marker = @closing_marker
|
|
277
|
-
|
|
282
|
+
@call_tasks << @task_runner.spawn do
|
|
278
283
|
answer_call(message, process, closing_marker)
|
|
279
284
|
rescue => error
|
|
280
285
|
@call_mutex.synchronize { call_errors << error }
|
|
281
286
|
end
|
|
282
287
|
end
|
|
283
|
-
thread.report_on_exception = false
|
|
284
|
-
@call_mutex.synchronize { @call_threads << thread }
|
|
285
288
|
end
|
|
286
289
|
|
|
287
290
|
def append_output(value)
|
|
@@ -342,14 +345,14 @@ module LittleGhost
|
|
|
342
345
|
end
|
|
343
346
|
target, session, sandbox, workspace, directory = claimed
|
|
344
347
|
watchdog = cancel_watchdog(target)
|
|
345
|
-
|
|
346
|
-
|
|
348
|
+
tasks, call_errors = @call_mutex.synchronize do
|
|
349
|
+
current_tasks = @call_tasks
|
|
347
350
|
current_errors = @call_errors
|
|
348
351
|
@closing_marker[:closing] = true
|
|
349
|
-
@
|
|
352
|
+
@call_tasks = []
|
|
350
353
|
@call_errors = []
|
|
351
354
|
@closing_marker = {closing: false}
|
|
352
|
-
[
|
|
355
|
+
[current_tasks, current_errors]
|
|
353
356
|
end
|
|
354
357
|
first_error = nil
|
|
355
358
|
begin
|
|
@@ -358,17 +361,19 @@ module LittleGhost
|
|
|
358
361
|
first_error ||= error
|
|
359
362
|
end
|
|
360
363
|
deadline = monotonic_time + @limits.fetch(:cleanup_seconds)
|
|
361
|
-
|
|
364
|
+
tasks.each do |task|
|
|
362
365
|
remaining = deadline - monotonic_time
|
|
363
366
|
break unless remaining.positive?
|
|
364
367
|
|
|
365
368
|
begin
|
|
366
|
-
|
|
369
|
+
task.wait(deadline: Time.now + remaining)
|
|
370
|
+
rescue DeadlineExceededError
|
|
371
|
+
break
|
|
367
372
|
rescue => error
|
|
368
373
|
first_error ||= error
|
|
369
374
|
end
|
|
370
375
|
end
|
|
371
|
-
if
|
|
376
|
+
if tasks.any?(&:alive?)
|
|
372
377
|
@closed = true
|
|
373
378
|
first_error ||= CleanupError.new("Code-mode nested tool cleanup timed out")
|
|
374
379
|
end
|
|
@@ -389,14 +394,14 @@ module LittleGhost
|
|
|
389
394
|
rescue => error
|
|
390
395
|
first_error ||= error
|
|
391
396
|
end
|
|
392
|
-
watchdog&.
|
|
397
|
+
watchdog&.wait unless watchdog&.current?
|
|
393
398
|
raise first_error if first_error
|
|
394
399
|
end
|
|
395
400
|
|
|
396
401
|
def start_watchdog(generation, deadline)
|
|
397
402
|
@watchdog_mutex.synchronize do
|
|
398
403
|
@watchdog_generation = generation
|
|
399
|
-
@watchdog =
|
|
404
|
+
@watchdog = @task_runner.spawn do
|
|
400
405
|
expired = @watchdog_mutex.synchronize do
|
|
401
406
|
loop do
|
|
402
407
|
break false unless @watchdog_generation.equal?(generation)
|
|
@@ -411,7 +416,6 @@ module LittleGhost
|
|
|
411
416
|
rescue => error
|
|
412
417
|
record_program_error(generation, error)
|
|
413
418
|
end
|
|
414
|
-
@watchdog.report_on_exception = false
|
|
415
419
|
end
|
|
416
420
|
end
|
|
417
421
|
|
|
@@ -419,11 +423,11 @@ module LittleGhost
|
|
|
419
423
|
@watchdog_mutex.synchronize do
|
|
420
424
|
return unless !generation || @watchdog_generation.equal?(generation)
|
|
421
425
|
|
|
422
|
-
|
|
426
|
+
task = @watchdog
|
|
423
427
|
@watchdog_generation = nil
|
|
424
428
|
@watchdog = nil
|
|
425
429
|
@watchdog_condition.broadcast
|
|
426
|
-
|
|
430
|
+
task
|
|
427
431
|
end
|
|
428
432
|
end
|
|
429
433
|
|
|
@@ -58,6 +58,7 @@ module LittleGhost
|
|
|
58
58
|
Output and completion:
|
|
59
59
|
- The final Ruby expression becomes the completed program value.
|
|
60
60
|
- Use `text(value)` for user-visible output.
|
|
61
|
+
- Ordinary `puts`, `print`, `printf`, and `p` output is captured and combined into bounded chunks.
|
|
61
62
|
- Use `finish(value)` to complete early with a value.
|
|
62
63
|
|
|
63
64
|
The Sandbox controls filesystem, network, subprocess, and optional-library access. Do not assume host
|
|
@@ -33,6 +33,7 @@ module LittleGhost
|
|
|
33
33
|
class Configuration
|
|
34
34
|
FILE_LOAD_MUTEX = Mutex.new # :nodoc:
|
|
35
35
|
CONFIGURATION_KEYS = %i[invocation service_name].freeze # :nodoc:
|
|
36
|
+
RUNTIME_BUILD_CONTEXT_KEY = :little_ghost_runtime_build_context # :nodoc:
|
|
36
37
|
DEFAULT_PROMPT_PATHS = ["app/prompts"].freeze # :nodoc:
|
|
37
38
|
DEFAULT_SKILL_PATHS = ["app/skills"].freeze # :nodoc:
|
|
38
39
|
##
|
|
@@ -97,8 +98,18 @@ module LittleGhost
|
|
|
97
98
|
sandbox: nil,
|
|
98
99
|
code_mode: nil,
|
|
99
100
|
instrumentation_subscribers: [],
|
|
100
|
-
runtime_hooks: []
|
|
101
|
+
runtime_hooks: [],
|
|
102
|
+
concurrency_backend: :auto
|
|
101
103
|
}.merge(values)
|
|
104
|
+
@configuration_values[:concurrency_backend] = normalize_concurrency_backend(
|
|
105
|
+
@configuration_values[:concurrency_backend]
|
|
106
|
+
)
|
|
107
|
+
if values.key?(:blocking_pool_capacity)
|
|
108
|
+
Support::Executor.blocking.runner.capacity = normalize_blocking_pool_capacity(
|
|
109
|
+
values[:blocking_pool_capacity]
|
|
110
|
+
)
|
|
111
|
+
@configuration_values.delete(:blocking_pool_capacity)
|
|
112
|
+
end
|
|
102
113
|
@configuration_values[:prompt_paths] = Array(@configuration_values[:prompt_paths]).dup
|
|
103
114
|
@configuration_values[:skill_paths] = Array(@configuration_values[:skill_paths]).dup
|
|
104
115
|
@configuration_values[:skill_resource_root] = Skills::ResourceRoot.normalize(
|
|
@@ -136,11 +147,11 @@ module LittleGhost
|
|
|
136
147
|
# other writes are rejected. A failed build leaves the configuration
|
|
137
148
|
# editable for a later attempt.
|
|
138
149
|
def runtime
|
|
139
|
-
build_generation = @lifecycle_monitor.synchronize do
|
|
150
|
+
build_generation, build_context = @lifecycle_monitor.synchronize do
|
|
140
151
|
loop do
|
|
141
152
|
return @default_runtime if @default_runtime
|
|
142
153
|
if @runtime_building
|
|
143
|
-
if
|
|
154
|
+
if current_runtime_build_context.equal?(@runtime_build_context)
|
|
144
155
|
raise ConfigurationError, "LittleGhost.runtime cannot be called while the shared Runtime is starting"
|
|
145
156
|
end
|
|
146
157
|
|
|
@@ -154,14 +165,14 @@ module LittleGhost
|
|
|
154
165
|
sealed_values = freeze_configuration_copy(@configuration_values)
|
|
155
166
|
@runtime_generation = @runtime_generation.to_i + 1
|
|
156
167
|
@runtime_building = true
|
|
157
|
-
@
|
|
168
|
+
@runtime_build_context = Object.new
|
|
158
169
|
@configuration_values = sealed_values
|
|
159
|
-
break @runtime_generation
|
|
170
|
+
break [@runtime_generation, @runtime_build_context]
|
|
160
171
|
end
|
|
161
172
|
end
|
|
162
173
|
end
|
|
163
174
|
|
|
164
|
-
|
|
175
|
+
ExecutionState.with(RUNTIME_BUILD_CONTEXT_KEY => build_context) do
|
|
165
176
|
runtime_root = root
|
|
166
177
|
load_file!(root: runtime_root)
|
|
167
178
|
runtime_settings = settings(root: runtime_root)
|
|
@@ -186,12 +197,70 @@ module LittleGhost
|
|
|
186
197
|
ensure
|
|
187
198
|
@lifecycle_monitor.synchronize do
|
|
188
199
|
@runtime_building = false
|
|
189
|
-
@
|
|
200
|
+
@runtime_build_context = nil
|
|
190
201
|
@runtime_condition.broadcast
|
|
191
202
|
end
|
|
192
203
|
end
|
|
193
204
|
end
|
|
194
205
|
|
|
206
|
+
# Selects how subsequently built runtimes start independent work such as
|
|
207
|
+
# parallel Tool calls and Workflow branches.
|
|
208
|
+
#
|
|
209
|
+
# The default, +:auto+, uses fibers when the caller is already running in a
|
|
210
|
+
# scheduled fiber and uses threads otherwise. +:thread+ always uses threads.
|
|
211
|
+
# +:fiber+ raises ConfigurationError when the caller is not in a scheduled
|
|
212
|
+
# fiber. The application's scheduler must support Fiber.schedule. Any other
|
|
213
|
+
# value raises ArgumentError.
|
|
214
|
+
#
|
|
215
|
+
# LittleGhost.configure do |config|
|
|
216
|
+
# config.concurrency_backend = :thread
|
|
217
|
+
# end
|
|
218
|
+
#
|
|
219
|
+
# :call-seq:
|
|
220
|
+
# concurrency_backend() -> :auto, :thread, :fiber
|
|
221
|
+
# concurrency_backend(value) -> :auto, :thread, :fiber
|
|
222
|
+
def concurrency_backend(value = :__read__)
|
|
223
|
+
return configuration_values[:concurrency_backend] if value == :__read__
|
|
224
|
+
|
|
225
|
+
normalized = normalize_concurrency_backend(value)
|
|
226
|
+
change_configuration { configuration_values[:concurrency_backend] = normalized }
|
|
227
|
+
normalized
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Replaces the concurrency backend for subsequently built runtimes.
|
|
231
|
+
def concurrency_backend=(value)
|
|
232
|
+
concurrency_backend(value)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Returns or sets the maximum number of process-wide workers available to
|
|
236
|
+
# LittleGhost.offload_blocking, certificate generation, and Filesystem
|
|
237
|
+
# SessionStore transactions when they run from scheduled fibers. Workers
|
|
238
|
+
# are created lazily. The default is 2. Every Configuration reads and writes
|
|
239
|
+
# the same process-wide value.
|
|
240
|
+
#
|
|
241
|
+
# Configure this during process startup, before any operation can start the
|
|
242
|
+
# pool. +value+ must be a positive Integer. Raises ArgumentError for an
|
|
243
|
+
# invalid value and ConfigurationError when changing the value after the
|
|
244
|
+
# pool has started.
|
|
245
|
+
#
|
|
246
|
+
# :call-seq:
|
|
247
|
+
# blocking_pool_capacity() -> integer
|
|
248
|
+
# blocking_pool_capacity(value) -> integer
|
|
249
|
+
def blocking_pool_capacity(value = :__read__)
|
|
250
|
+
return Support::Executor.blocking.runner.capacity if value == :__read__
|
|
251
|
+
|
|
252
|
+
normalized = normalize_blocking_pool_capacity(value)
|
|
253
|
+
change_configuration do
|
|
254
|
+
Support::Executor.blocking.runner.capacity = normalized
|
|
255
|
+
end
|
|
256
|
+
normalized
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Sets the same process-wide worker limit as +blocking_pool_capacity+.
|
|
260
|
+
def blocking_pool_capacity=(value)
|
|
261
|
+
blocking_pool_capacity(value)
|
|
262
|
+
end
|
|
263
|
+
|
|
195
264
|
# Workspace declaration used for subsequently built runtimes.
|
|
196
265
|
def workspace = configuration_values[:workspace]
|
|
197
266
|
# Sandbox declaration used for subsequently built runtimes.
|
|
@@ -419,6 +488,8 @@ module LittleGhost
|
|
|
419
488
|
|
|
420
489
|
# Looks up an arbitrary setting by symbol or string-compatible name.
|
|
421
490
|
def [](name)
|
|
491
|
+
return blocking_pool_capacity if name.to_sym == :blocking_pool_capacity
|
|
492
|
+
|
|
422
493
|
configuration_values.fetch(name.to_sym)
|
|
423
494
|
end
|
|
424
495
|
|
|
@@ -431,6 +502,10 @@ module LittleGhost
|
|
|
431
502
|
self.sandbox = value
|
|
432
503
|
when :code_mode
|
|
433
504
|
self.code_mode = value
|
|
505
|
+
when :concurrency_backend
|
|
506
|
+
self.concurrency_backend = value
|
|
507
|
+
when :blocking_pool_capacity
|
|
508
|
+
self.blocking_pool_capacity = value
|
|
434
509
|
else
|
|
435
510
|
change_configuration { configuration_values[name.to_sym] = value }
|
|
436
511
|
end
|
|
@@ -615,7 +690,7 @@ module LittleGhost
|
|
|
615
690
|
|
|
616
691
|
def configuration_values
|
|
617
692
|
@lifecycle_monitor.synchronize do
|
|
618
|
-
if @runtime_building &&
|
|
693
|
+
if @runtime_building && !current_runtime_build_context.equal?(@runtime_build_context)
|
|
619
694
|
raise_locked_configuration!
|
|
620
695
|
end
|
|
621
696
|
|
|
@@ -701,7 +776,24 @@ module LittleGhost
|
|
|
701
776
|
return false if @default_runtime
|
|
702
777
|
return true unless @runtime_building
|
|
703
778
|
|
|
704
|
-
@configuration_file_loading &&
|
|
779
|
+
@configuration_file_loading && current_runtime_build_context.equal?(@runtime_build_context)
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
def current_runtime_build_context
|
|
783
|
+
ExecutionState[RUNTIME_BUILD_CONTEXT_KEY]
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
def normalize_concurrency_backend(value)
|
|
787
|
+
normalized = value.to_sym if value.respond_to?(:to_sym)
|
|
788
|
+
return normalized if Support::TaskRunner::BACKENDS.include?(normalized)
|
|
789
|
+
|
|
790
|
+
raise ArgumentError, "concurrency_backend must be :auto, :thread, or :fiber"
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
def normalize_blocking_pool_capacity(value)
|
|
794
|
+
return value if value.is_a?(Integer) && value.positive?
|
|
795
|
+
|
|
796
|
+
raise ArgumentError, "blocking_pool_capacity must be a positive integer"
|
|
705
797
|
end
|
|
706
798
|
|
|
707
799
|
def load_configuration_file(path)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LittleGhost
|
|
4
|
+
# Carries provider-neutral inputs and results for text embeddings.
|
|
5
|
+
#
|
|
6
|
+
# Applications usually call LittleGhost.embed. Provider adapters receive a
|
|
7
|
+
# Request and return a Response without exposing provider response objects.
|
|
8
|
+
module Embeddings
|
|
9
|
+
DEFAULT_LIMITS = {
|
|
10
|
+
max_inputs: 128,
|
|
11
|
+
max_input_bytes: 64 * 1024,
|
|
12
|
+
max_total_bytes: 1024 * 1024
|
|
13
|
+
}.freeze # :nodoc:
|
|
14
|
+
|
|
15
|
+
Request = Data.define(:inputs, :settings, :limits, :cancellation_token, :deadline) do # :nodoc:
|
|
16
|
+
# Validates configured budgets before copying retained input strings.
|
|
17
|
+
def initialize(inputs:, settings: {}, limits: {}, cancellation_token: Support::CancellationToken.new, deadline: nil)
|
|
18
|
+
values = inputs.is_a?(String) ? [inputs] : inputs
|
|
19
|
+
unless values.is_a?(Array) && !values.empty? && values.all? { |value| value.is_a?(String) && !value.empty? }
|
|
20
|
+
raise ArgumentError, "inputs must be a nonempty String or Array of nonempty Strings"
|
|
21
|
+
end
|
|
22
|
+
raise ArgumentError, "settings must be a mapping" unless settings.respond_to?(:to_h)
|
|
23
|
+
raise ArgumentError, "limits must be a mapping" unless limits.respond_to?(:to_h)
|
|
24
|
+
|
|
25
|
+
configured_limits = DEFAULT_LIMITS.merge(limits.to_h.transform_keys(&:to_sym))
|
|
26
|
+
configured_limits.transform_values! { |value| Integer(value) }
|
|
27
|
+
unless configured_limits.values.all?(&:positive?)
|
|
28
|
+
raise ArgumentError, "embedding limits must be positive integers"
|
|
29
|
+
end
|
|
30
|
+
if values.length > configured_limits.fetch(:max_inputs)
|
|
31
|
+
raise UnsupportedInputError, "Embedding input count exceeds the configured limit"
|
|
32
|
+
end
|
|
33
|
+
total_bytes = 0
|
|
34
|
+
values.each do |value|
|
|
35
|
+
bytes = value.bytesize
|
|
36
|
+
if bytes > configured_limits.fetch(:max_input_bytes)
|
|
37
|
+
raise UnsupportedInputError, "An embedding input exceeds the configured byte limit"
|
|
38
|
+
end
|
|
39
|
+
total_bytes += bytes
|
|
40
|
+
if total_bytes > configured_limits.fetch(:max_total_bytes)
|
|
41
|
+
raise UnsupportedInputError, "Embedding inputs exceed the configured aggregate byte limit"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
super(
|
|
46
|
+
inputs: values.map { |value| value.dup.freeze }.freeze,
|
|
47
|
+
settings: settings.to_h.transform_keys(&:to_sym).freeze,
|
|
48
|
+
limits: configured_limits.freeze,
|
|
49
|
+
cancellation_token:,
|
|
50
|
+
deadline:
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Carries the validated inputs and controls for one embedding operation.
|
|
56
|
+
#
|
|
57
|
+
# LittleGhost builds this value for LittleGhost.embed and passes it to the
|
|
58
|
+
# selected provider. Provider implementations may receive calls
|
|
59
|
+
# concurrently. They must observe +cancellation_token+ and +deadline+ while
|
|
60
|
+
# performing external work.
|
|
61
|
+
class Request < Data # :doc:
|
|
62
|
+
##
|
|
63
|
+
# :singleton-method: new
|
|
64
|
+
# :call-seq:
|
|
65
|
+
# new(inputs:, settings: {}, limits: {}, cancellation_token: Support::CancellationToken.new, deadline: nil) -> Request
|
|
66
|
+
#
|
|
67
|
+
# Validates and freezes one String or a nonempty Array of Strings.
|
|
68
|
+
#
|
|
69
|
+
# +settings+ contains trusted model settings. +limits+ may override the
|
|
70
|
+
# input budgets:
|
|
71
|
+
#
|
|
72
|
+
# [+:max_inputs+]
|
|
73
|
+
# Number of input strings. Defaults to 128.
|
|
74
|
+
# [+:max_input_bytes+]
|
|
75
|
+
# Bytes allowed in one input. Defaults to 64 KiB.
|
|
76
|
+
# [+:max_total_bytes+]
|
|
77
|
+
# Bytes allowed across all inputs. Defaults to 1 MiB.
|
|
78
|
+
#
|
|
79
|
+
# Invalid shapes raise ArgumentError; inputs outside these budgets raise
|
|
80
|
+
# UnsupportedInputError.
|
|
81
|
+
|
|
82
|
+
##
|
|
83
|
+
# :attr_reader: inputs
|
|
84
|
+
# Frozen input strings in caller order.
|
|
85
|
+
|
|
86
|
+
##
|
|
87
|
+
# :attr_reader: settings
|
|
88
|
+
# Frozen model settings chosen by trusted application code.
|
|
89
|
+
|
|
90
|
+
##
|
|
91
|
+
# :attr_reader: limits
|
|
92
|
+
# Frozen input budgets applied before LittleGhost retains the strings.
|
|
93
|
+
|
|
94
|
+
##
|
|
95
|
+
# :attr_reader: cancellation_token
|
|
96
|
+
# Token that stops the provider operation when cancellation is requested.
|
|
97
|
+
|
|
98
|
+
##
|
|
99
|
+
# :attr_reader: deadline
|
|
100
|
+
# Monotonic deadline for the provider operation, or +nil+.
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LittleGhost
|
|
4
|
+
module Embeddings
|
|
5
|
+
Response = Data.define(:vectors, :usage, :metadata) do # :nodoc:
|
|
6
|
+
# Validates finite, consistently sized numeric vectors and freezes them.
|
|
7
|
+
def initialize(vectors:, usage: Usage.new, metadata: {})
|
|
8
|
+
values = Array(vectors)
|
|
9
|
+
unless !values.empty? && values.all? { |vector| vector.is_a?(Array) && !vector.empty? }
|
|
10
|
+
raise ProtocolError, "Embedding provider returned no vectors"
|
|
11
|
+
end
|
|
12
|
+
dimensions = values.first.length
|
|
13
|
+
unless values.all? { |vector| vector.length == dimensions && vector.all? { |number| number.is_a?(Numeric) && (!number.respond_to?(:finite?) || number.finite?) } }
|
|
14
|
+
raise ProtocolError, "Embedding provider returned invalid vectors"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
frozen_vectors = values.map { |vector| vector.map(&:to_f).freeze }.freeze
|
|
18
|
+
super(vectors: frozen_vectors, usage:, metadata: metadata.to_h.merge(dimensions:).freeze)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Number of numeric values in each vector.
|
|
22
|
+
def dimensions = metadata.fetch(:dimensions)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Carries validated embedding vectors without provider response objects.
|
|
26
|
+
#
|
|
27
|
+
# Vectors are finite, consistently sized, frozen, and ordered like the
|
|
28
|
+
# corresponding Request inputs. LittleGhost raises ProtocolError instead of
|
|
29
|
+
# returning an invalid or partial provider response.
|
|
30
|
+
class Response < Data # :doc:
|
|
31
|
+
##
|
|
32
|
+
# :singleton-method: new
|
|
33
|
+
# :call-seq:
|
|
34
|
+
# new(vectors:, usage: Usage.new, metadata: {}) -> Response
|
|
35
|
+
#
|
|
36
|
+
# Validates and freezes provider-neutral vectors, usage, and metadata.
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# :attr_reader: vectors
|
|
40
|
+
# Frozen numeric vectors in request-input order.
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# :attr_reader: usage
|
|
44
|
+
# Normalized provider usage for the operation.
|
|
45
|
+
|
|
46
|
+
##
|
|
47
|
+
# :attr_reader: metadata
|
|
48
|
+
# Frozen operation metadata, including +dimensions+.
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# :method: dimensions
|
|
52
|
+
# Number of numeric values in each vector.
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/little_ghost/errors.rb
CHANGED
|
@@ -36,6 +36,8 @@ module LittleGhost
|
|
|
36
36
|
class AssemblyStepTimeoutError < AssemblyError; end
|
|
37
37
|
# Raised when an invocation contains an unsupported input form.
|
|
38
38
|
class UnsupportedInputError < InvocationError; end
|
|
39
|
+
# Raised when a provider does not implement the requested model operation.
|
|
40
|
+
class UnsupportedModelOperationError < ConfigurationError; end
|
|
39
41
|
# Base class for provider request, response, and protocol failures. Agent Runs
|
|
40
42
|
# normally record these as failed outcomes; provider retry policy may handle a
|
|
41
43
|
# retryable failure before it reaches the Run.
|