little_ghost 0.3.0 → 0.4.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 +68 -84
- data/docs/guides/assemblies.md +286 -0
- data/docs/guides/core_concepts.md +126 -231
- data/docs/guides/getting_started.md +114 -87
- data/docs/guides/production.md +187 -0
- data/docs/guides/prompt_views.md +132 -0
- data/lib/little_ghost/ag_ui/adapter.rb +3 -3
- data/lib/little_ghost/agent/delegation.rb +1 -1
- data/lib/little_ghost/agent.rb +167 -172
- data/lib/little_ghost/{agent_interruptions.rb → agent_interjections.rb} +12 -12
- data/lib/little_ghost/assembly.rb +55 -21
- data/lib/little_ghost/assembly_builder.rb +40 -2
- data/lib/little_ghost/assembly_execution.rb +87 -4
- data/lib/little_ghost/configuration.rb +263 -39
- data/lib/little_ghost/content.rb +5 -5
- data/lib/little_ghost/data_map.rb +209 -0
- data/lib/little_ghost/errors.rb +2 -2
- data/lib/little_ghost/execution.rb +32 -32
- data/lib/little_ghost/graph.rb +22 -3
- data/lib/little_ghost/message.rb +4 -4
- data/lib/little_ghost/model_resolver.rb +2 -2
- data/lib/little_ghost/prompt_resolver.rb +2 -0
- data/lib/little_ghost/run.rb +87 -49
- data/lib/little_ghost/run_context.rb +33 -20
- data/lib/little_ghost/runtime/hook.rb +3 -3
- data/lib/little_ghost/runtime.rb +71 -31
- data/lib/little_ghost/session.rb +12 -23
- 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/subagents/manager.rb +42 -42
- data/lib/little_ghost/swarm.rb +13 -5
- data/lib/little_ghost/tool.rb +56 -14
- 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.rb +29 -25
- metadata +7 -2
data/lib/little_ghost/run.rb
CHANGED
|
@@ -12,30 +12,68 @@ module LittleGhost
|
|
|
12
12
|
# run.outcome # => "completed"
|
|
13
13
|
# run.response # => "Transfer 481 is waiting for the receiving bank."
|
|
14
14
|
#
|
|
15
|
-
# The class-level ask helper[rdoc-ref:LittleGhost::Assembly.ask]
|
|
16
|
-
# ask method[rdoc-ref:LittleGhost::Assembly#ask]
|
|
17
|
-
#
|
|
15
|
+
# The class-level ask helper[rdoc-ref:LittleGhost::Assembly.ask] and standalone
|
|
16
|
+
# ask method[rdoc-ref:LittleGhost::Assembly#ask] return the Run after work
|
|
17
|
+
# finishes. For a live interface, use the class-level streaming
|
|
18
18
|
# helper[rdoc-ref:LittleGhost::Assembly.stream_ask] or standalone streaming
|
|
19
|
-
# method[rdoc-ref:LittleGhost::Assembly#stream_ask]
|
|
20
|
-
# and returns the same
|
|
19
|
+
# method[rdoc-ref:LittleGhost::Assembly#stream_ask]. The stream yields
|
|
20
|
+
# StreamEvent objects, and enumeration returns the same Run with its final
|
|
21
|
+
# outcome and response. A Run executes only once.
|
|
22
|
+
#
|
|
23
|
+
# stream = CustomerSupportAgent.stream_ask("Where is transfer 481?")
|
|
24
|
+
# run = stream.each do |event|
|
|
25
|
+
# publish(event) if event.type == :text_delta
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# run.completed? # => true
|
|
29
|
+
# run.response
|
|
21
30
|
#
|
|
22
31
|
# Completion, failure, deadline, and cancellation become the +completed+,
|
|
23
32
|
# +failed+, +partial+, and +cancelled+ outcomes. Ordinary execution failures
|
|
24
|
-
# are available through +error+ and the terminal stream event
|
|
25
|
-
#
|
|
26
|
-
# cannot
|
|
33
|
+
# are available through +error+ and the terminal stream event. Failures while
|
|
34
|
+
# closing resources, delivering events, or reporting instrumentation may
|
|
35
|
+
# still raise because LittleGhost cannot report a reliable ending.
|
|
36
|
+
#
|
|
37
|
+
# Tool validation and ToolError failures return safe Tool results to the model,
|
|
38
|
+
# which may recover and complete the Run. Input, configuration, or resource
|
|
39
|
+
# construction can raise before a Run exists. Once execution begins, terminal
|
|
40
|
+
# events are +run_stop+, +run_error+, +run_partial+, and +run_cancel+.
|
|
27
41
|
#
|
|
28
|
-
# The
|
|
29
|
-
# registered resources in reverse order. +register+
|
|
30
|
-
#
|
|
31
|
-
# entrypoint is active
|
|
42
|
+
# The Run opens its workspace, sandbox, Session, and Assembly entrypoint, then
|
|
43
|
+
# closes registered resources in reverse order. +register+ adds application
|
|
44
|
+
# resources to that lifecycle. Interjection is available only while one Agent
|
|
45
|
+
# entrypoint is active.
|
|
32
46
|
class Run
|
|
33
47
|
include Enumerable
|
|
34
48
|
|
|
35
|
-
# Runtime
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
49
|
+
# Runtime that built and executes this Run.
|
|
50
|
+
attr_reader :runtime
|
|
51
|
+
# Agent class used for compatibility when the entrypoint is an Agent.
|
|
52
|
+
attr_reader :agent_class
|
|
53
|
+
# Public Agent, Workflow, Swarm, or Graph class selected by the caller.
|
|
54
|
+
attr_reader :entrypoint_class
|
|
55
|
+
# Normalized request carried by this Run.
|
|
56
|
+
attr_reader :invocation
|
|
57
|
+
# Token that cooperatively stops this Run and its children.
|
|
58
|
+
attr_reader :cancellation_token
|
|
59
|
+
# Final RunResult, when the Assembly produced one.
|
|
60
|
+
attr_reader :result
|
|
61
|
+
# Unique identifier for this top-level operation.
|
|
62
|
+
attr_reader :operation_id
|
|
63
|
+
# Terminal String: +completed+, +failed+, +partial+, or +cancelled+.
|
|
64
|
+
attr_reader :outcome
|
|
65
|
+
# Caller-facing final text, or the partial text preserved at a deadline.
|
|
66
|
+
attr_reader :response
|
|
67
|
+
# Exception that caused a failed, partial, or cancelled outcome.
|
|
68
|
+
attr_reader :error
|
|
69
|
+
# Session opened for this invocation, when persistence is configured.
|
|
70
|
+
attr_reader :session
|
|
71
|
+
# Normalized Usage accumulated by the Run.
|
|
72
|
+
attr_reader :usage
|
|
73
|
+
# Request-scoped workspace owned or supplied by the Run.
|
|
74
|
+
attr_reader :workspace
|
|
75
|
+
# Request-scoped sandbox owned or supplied by the Run.
|
|
76
|
+
attr_reader :sandbox
|
|
39
77
|
|
|
40
78
|
# Creates a dormant run for +invocation+.
|
|
41
79
|
def initialize(invocation:, runtime:, agent_class: nil, assembly_class: nil, entrypoint_class: nil,
|
|
@@ -66,10 +104,10 @@ module LittleGhost
|
|
|
66
104
|
@exclusive_tools_mutex = Mutex.new
|
|
67
105
|
@once_mutex = Mutex.new
|
|
68
106
|
@once_keys = {}
|
|
69
|
-
@
|
|
70
|
-
@
|
|
71
|
-
@
|
|
72
|
-
@
|
|
107
|
+
@interjection_mutex = Mutex.new
|
|
108
|
+
@interjection_condition = ConditionVariable.new
|
|
109
|
+
@interjection_state = :not_started
|
|
110
|
+
@active_interjections = 0
|
|
73
111
|
@entrypoint = nil
|
|
74
112
|
@usage = Usage.new
|
|
75
113
|
end
|
|
@@ -108,23 +146,23 @@ module LittleGhost
|
|
|
108
146
|
# True when cancellation stopped the run without a response.
|
|
109
147
|
def cancelled? = outcome == "cancelled"
|
|
110
148
|
|
|
111
|
-
# Adds an
|
|
149
|
+
# Adds an interjection to the active entrypoint and waits for its response.
|
|
112
150
|
#
|
|
113
|
-
# Raises LittleGhost::
|
|
114
|
-
# after it finishes, or when the entrypoint does not support
|
|
115
|
-
def
|
|
151
|
+
# Raises LittleGhost::AgentInterjectionError before the entrypoint is ready,
|
|
152
|
+
# after it finishes, or when the entrypoint does not support interjections.
|
|
153
|
+
def interject(
|
|
116
154
|
message,
|
|
117
|
-
|
|
155
|
+
interjection_id: nil,
|
|
118
156
|
batch_key: nil,
|
|
119
157
|
metadata: {},
|
|
120
158
|
cancellation_token: Support::CancellationToken.new,
|
|
121
159
|
deadline: nil
|
|
122
160
|
)
|
|
123
|
-
|
|
161
|
+
interject_with do
|
|
124
162
|
[
|
|
125
163
|
message,
|
|
126
164
|
{
|
|
127
|
-
|
|
165
|
+
interjection_id:,
|
|
128
166
|
batch_key:,
|
|
129
167
|
metadata:,
|
|
130
168
|
cancellation_token:,
|
|
@@ -134,29 +172,29 @@ module LittleGhost
|
|
|
134
172
|
end
|
|
135
173
|
end
|
|
136
174
|
|
|
137
|
-
def
|
|
138
|
-
entrypoint = @
|
|
139
|
-
case @
|
|
175
|
+
def interject_with # :nodoc:
|
|
176
|
+
entrypoint = @interjection_mutex.synchronize do
|
|
177
|
+
case @interjection_state
|
|
140
178
|
when :not_started, :starting
|
|
141
|
-
raise
|
|
179
|
+
raise AgentInterjectionError, "Run entrypoint is not ready for interjections"
|
|
142
180
|
when :terminal
|
|
143
|
-
raise
|
|
181
|
+
raise AgentInterjectionError, "Run has already finished"
|
|
144
182
|
end
|
|
145
183
|
|
|
146
|
-
@
|
|
184
|
+
@active_interjections += 1
|
|
147
185
|
@entrypoint
|
|
148
186
|
end
|
|
149
|
-
unless entrypoint.respond_to?(:
|
|
150
|
-
raise
|
|
187
|
+
unless entrypoint.respond_to?(:interject)
|
|
188
|
+
raise AgentInterjectionError, "Run entrypoint does not support interjections"
|
|
151
189
|
end
|
|
152
190
|
|
|
153
191
|
message, options = yield
|
|
154
|
-
entrypoint.
|
|
192
|
+
entrypoint.interject(message, **options)
|
|
155
193
|
ensure
|
|
156
194
|
if entrypoint
|
|
157
|
-
@
|
|
158
|
-
@
|
|
159
|
-
@
|
|
195
|
+
@interjection_mutex.synchronize do
|
|
196
|
+
@active_interjections -= 1
|
|
197
|
+
@interjection_condition.broadcast
|
|
160
198
|
end
|
|
161
199
|
end
|
|
162
200
|
end
|
|
@@ -210,8 +248,8 @@ module LittleGhost
|
|
|
210
248
|
end
|
|
211
249
|
end
|
|
212
250
|
|
|
213
|
-
def
|
|
214
|
-
runtime.
|
|
251
|
+
def prepare_interjection(payload) # :nodoc:
|
|
252
|
+
runtime.prepare_interjection(self, payload)
|
|
215
253
|
end
|
|
216
254
|
|
|
217
255
|
# Closes registered resources in reverse order.
|
|
@@ -273,7 +311,7 @@ module LittleGhost
|
|
|
273
311
|
sandbox&.open(run: self)
|
|
274
312
|
@session = runtime.open_session(self)
|
|
275
313
|
agent = runtime.build_assembly(@execution_class, run: self)
|
|
276
|
-
@
|
|
314
|
+
@interjection_mutex.synchronize do
|
|
277
315
|
@entrypoint = agent
|
|
278
316
|
end
|
|
279
317
|
register(agent)
|
|
@@ -294,11 +332,11 @@ module LittleGhost
|
|
|
294
332
|
end
|
|
295
333
|
}
|
|
296
334
|
if agent.is_a?(Agent)
|
|
297
|
-
options[:
|
|
298
|
-
@
|
|
335
|
+
options[:interject_ready] = lambda do
|
|
336
|
+
@interjection_mutex.synchronize { @interjection_state = :active }
|
|
299
337
|
end
|
|
300
338
|
else
|
|
301
|
-
@
|
|
339
|
+
@interjection_mutex.synchronize { @interjection_state = :active }
|
|
302
340
|
end
|
|
303
341
|
|
|
304
342
|
agent.stream(invocation.message, **options).each do |event|
|
|
@@ -359,9 +397,9 @@ module LittleGhost
|
|
|
359
397
|
}
|
|
360
398
|
]
|
|
361
399
|
ensure
|
|
362
|
-
@
|
|
363
|
-
@
|
|
364
|
-
@
|
|
400
|
+
@interjection_mutex.synchronize do
|
|
401
|
+
@interjection_state = :terminal
|
|
402
|
+
@interjection_condition.wait(@interjection_mutex) while @active_interjections.positive?
|
|
365
403
|
@last_entrypoint = @entrypoint
|
|
366
404
|
@entrypoint = nil
|
|
367
405
|
end
|
|
@@ -631,7 +669,7 @@ module LittleGhost
|
|
|
631
669
|
raise Error, "run has already started" if @started
|
|
632
670
|
@started = true
|
|
633
671
|
end
|
|
634
|
-
@
|
|
672
|
+
@interjection_mutex.synchronize { @interjection_state = :starting }
|
|
635
673
|
end
|
|
636
674
|
|
|
637
675
|
def close_callback(resource)
|
|
@@ -9,12 +9,25 @@ module LittleGhost
|
|
|
9
9
|
# cancellation and deadlines, checkpoint messages, and accumulate usage.
|
|
10
10
|
# Access to framework-managed fields is thread-safe.
|
|
11
11
|
class RunContext
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
#
|
|
12
|
+
# Mutable DataMap state supplied to this invocation. A top-level Run starts
|
|
13
|
+
# with restored Session state merged with current Invocation context; child
|
|
14
|
+
# Assemblies may receive copied, mapped, or empty state. Application code must
|
|
15
|
+
# synchronize mutations when parallel Tools share this map, or use exclusive
|
|
16
|
+
# Tools. String and Symbol keys address the same value; persisted snapshots
|
|
17
|
+
# use canonical String keys.
|
|
18
|
+
attr_reader :state
|
|
19
|
+
# Token used to cooperatively stop the current work.
|
|
20
|
+
attr_reader :cancellation_token
|
|
21
|
+
# Wall-clock deadline for the current work, when present.
|
|
22
|
+
attr_reader :deadline
|
|
23
|
+
# Framework metadata attached to this context.
|
|
24
|
+
attr_reader :metadata
|
|
25
|
+
# Active Agent operation identifier, after the context is bound.
|
|
26
|
+
attr_reader :agent_operation_id
|
|
27
|
+
# Durable subagent conversation identifier, when present.
|
|
28
|
+
attr_reader :conversation_id
|
|
29
|
+
|
|
30
|
+
# Creates a context with optional checkpoint and interjection state.
|
|
18
31
|
def initialize(
|
|
19
32
|
state: {},
|
|
20
33
|
cancellation_token: Support::CancellationToken.new,
|
|
@@ -22,15 +35,15 @@ module LittleGhost
|
|
|
22
35
|
metadata: {},
|
|
23
36
|
checkpoint: nil,
|
|
24
37
|
conversation_id: nil,
|
|
25
|
-
|
|
26
|
-
|
|
38
|
+
interjection_metadata: nil,
|
|
39
|
+
interjection_ids: []
|
|
27
40
|
)
|
|
28
41
|
if conversation_id
|
|
29
42
|
conversation_id = String(conversation_id)
|
|
30
43
|
raise ArgumentError, "conversation_id cannot be empty" if conversation_id.empty?
|
|
31
44
|
conversation_id = conversation_id.dup.freeze
|
|
32
45
|
end
|
|
33
|
-
@state = state
|
|
46
|
+
@state = DataMap.new(state)
|
|
34
47
|
@cancellation_token = cancellation_token
|
|
35
48
|
@deadline = deadline
|
|
36
49
|
@metadata = metadata.freeze
|
|
@@ -42,9 +55,9 @@ module LittleGhost
|
|
|
42
55
|
@structured_result_mutex = Mutex.new
|
|
43
56
|
@agent_operation_id = nil
|
|
44
57
|
@agent_operation_id_mutex = Mutex.new
|
|
45
|
-
@
|
|
46
|
-
@
|
|
47
|
-
@
|
|
58
|
+
@interjection_mutex = Mutex.new
|
|
59
|
+
@interjection_metadata = interjection_metadata&.to_h
|
|
60
|
+
@interjection_ids = Array(interjection_ids).map { |id| String(id).dup.freeze }.freeze
|
|
48
61
|
end
|
|
49
62
|
|
|
50
63
|
# Raises LittleGhost::CancelledError or LittleGhost::DeadlineExceededError
|
|
@@ -99,20 +112,20 @@ module LittleGhost
|
|
|
99
112
|
@structured_result_mutex.synchronize { @structured_result }
|
|
100
113
|
end
|
|
101
114
|
|
|
102
|
-
def
|
|
103
|
-
@
|
|
115
|
+
def interjection_metadata # :nodoc:
|
|
116
|
+
@interjection_mutex.synchronize { @interjection_metadata }
|
|
104
117
|
end
|
|
105
118
|
|
|
106
|
-
def
|
|
107
|
-
@
|
|
119
|
+
def interjection_ids # :nodoc:
|
|
120
|
+
@interjection_mutex.synchronize { @interjection_ids }
|
|
108
121
|
end
|
|
109
122
|
|
|
110
|
-
def
|
|
123
|
+
def activate_interjection(metadata:, ids:) # :nodoc:
|
|
111
124
|
value = metadata&.to_h
|
|
112
125
|
values = Array(ids).map { |id| String(id).dup.freeze }.freeze
|
|
113
|
-
@
|
|
114
|
-
@
|
|
115
|
-
@
|
|
126
|
+
@interjection_mutex.synchronize do
|
|
127
|
+
@interjection_metadata = value
|
|
128
|
+
@interjection_ids = values
|
|
116
129
|
end
|
|
117
130
|
end
|
|
118
131
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module LittleGhost
|
|
4
4
|
class Runtime
|
|
5
5
|
# Hooks let applications prepare runs, select session history, transform
|
|
6
|
-
#
|
|
6
|
+
# interjections, and map errors to caller-safe messages.
|
|
7
7
|
#
|
|
8
8
|
# Hooks are instantiated once per Runtime in configuration order. Override
|
|
9
9
|
# only the methods needed and return the supplied value when leaving it
|
|
@@ -20,8 +20,8 @@ module LittleGhost
|
|
|
20
20
|
# lifecycle and close in reverse order.
|
|
21
21
|
def prepare_run(run) = run
|
|
22
22
|
|
|
23
|
-
# Transforms an
|
|
24
|
-
def
|
|
23
|
+
# Transforms an interjection payload before it reaches the agent.
|
|
24
|
+
def prepare_interjection(_run, payload) = payload
|
|
25
25
|
|
|
26
26
|
# Returns the history to use for this run, or nil to defer to later hooks
|
|
27
27
|
# and the session default. +stored+ is empty when the session is new;
|
data/lib/little_ghost/runtime.rb
CHANGED
|
@@ -4,45 +4,86 @@ require "json"
|
|
|
4
4
|
require_relative "configuration"
|
|
5
5
|
|
|
6
6
|
module LittleGhost
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# Owns the shared services that assemblies reuse across many Runs.
|
|
8
|
+
#
|
|
9
|
+
# Most applications do not construct this class. Configure LittleGhost once
|
|
10
|
+
# and call a named Agent or Assembly; the first standalone call lazily builds
|
|
11
|
+
# +LittleGhost.runtime+, and later calls reuse it automatically. Each call
|
|
12
|
+
# still receives a fresh Run, bound participants, Tools, workspace, and
|
|
13
|
+
# sandbox.
|
|
14
|
+
#
|
|
15
|
+
# Construct Runtime directly when one process intentionally hosts an isolated
|
|
16
|
+
# LittleGhost setup:
|
|
10
17
|
#
|
|
11
18
|
# configuration = LittleGhost::Configuration.new(
|
|
12
19
|
# root: Dir.pwd,
|
|
13
20
|
# providers: {
|
|
14
|
-
#
|
|
21
|
+
# openrouter: {adapter: :openrouter, api_key: ENV.fetch("OPENROUTER_API_KEY")}
|
|
15
22
|
# },
|
|
16
|
-
# models: {customer_support: {target: "openai
|
|
17
|
-
# default_model:
|
|
23
|
+
# models: {customer_support: {target: "openrouter:openai/gpt-5.6-luna"}},
|
|
24
|
+
# default_model: :customer_support,
|
|
18
25
|
# service_name: "support-api"
|
|
19
26
|
# )
|
|
20
27
|
# runtime = LittleGhost::Runtime.new(configuration: configuration)
|
|
21
28
|
#
|
|
22
|
-
# runtime
|
|
23
|
-
#
|
|
29
|
+
# CustomerSupportAgent.new(runtime: runtime)
|
|
30
|
+
# .ask("Where is order 481?")
|
|
31
|
+
# .response
|
|
32
|
+
#
|
|
33
|
+
# Explicit construction snapshots the supplied Configuration but does not
|
|
34
|
+
# replace LittleGhost's shared default Runtime.
|
|
35
|
+
#
|
|
36
|
+
# A Runtime may build independent Runs concurrently. Each Run gets its own
|
|
37
|
+
# bound participants and Tools. By default, Runtime also creates a workspace
|
|
38
|
+
# and sandbox owned by that Run. Existing workspace or sandbox instances
|
|
39
|
+
# passed by the application remain caller-owned. Other supplied objects may
|
|
40
|
+
# receive calls from several threads, so custom stores, resolvers, hooks,
|
|
41
|
+
# subscribers, providers, and resource factories must be thread-safe. One
|
|
42
|
+
# SessionStore instance serializes calls sharing a Session; multi-process
|
|
43
|
+
# deployments need coordination provided by their store.
|
|
44
|
+
#
|
|
45
|
+
# == Advanced construction and ownership
|
|
24
46
|
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
# lower-level path used to create a sibling runtime from an existing snapshot.
|
|
47
|
+
# Normal construction reads the application's configured definitions and
|
|
48
|
+
# builds shared model resolution, persistence, hooks, and resource factories.
|
|
49
|
+
# The +settings+ form and #build are lower-level extension points for deriving
|
|
50
|
+
# another Runtime from an existing configuration snapshot.
|
|
30
51
|
#
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
52
|
+
# #build_run creates a workspace and sandbox when needed. Once the Run owns
|
|
53
|
+
# them, it closes them; if construction stops halfway through, Runtime closes
|
|
54
|
+
# the partial resources. Startup failures are reported to instrumentation and
|
|
55
|
+
# then raised. Session actor resolution must use authenticated application
|
|
56
|
+
# identity. The default UnrestrictedSandbox uses host permissions and is not a
|
|
57
|
+
# security boundary for untrusted work.
|
|
35
58
|
#
|
|
36
|
-
#
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
# isolation. The default UnrestrictedSandbox is convenient application plumbing,
|
|
40
|
-
# not a security boundary for untrusted work.
|
|
59
|
+
# Runtime has no shutdown operation. Runs close resources created for their
|
|
60
|
+
# request. The application shuts down shared services and process-wide
|
|
61
|
+
# Instrumentation subscribers with the rest of the process.
|
|
41
62
|
class Runtime
|
|
42
|
-
#
|
|
43
|
-
attr_reader :configuration
|
|
44
|
-
|
|
45
|
-
|
|
63
|
+
# Configuration object used to construct this Runtime.
|
|
64
|
+
attr_reader :configuration
|
|
65
|
+
# Settings snapshot used by new Runs.
|
|
66
|
+
attr_reader :settings
|
|
67
|
+
# Canonical application root.
|
|
68
|
+
attr_reader :root
|
|
69
|
+
# Loader used for conventional application definitions.
|
|
70
|
+
attr_reader :loader
|
|
71
|
+
# Ordered directories searched for prompt templates.
|
|
72
|
+
attr_reader :prompt_paths
|
|
73
|
+
# Ordered directories searched for skill definitions.
|
|
74
|
+
attr_reader :skill_paths
|
|
75
|
+
# Root used for skill-owned resources, when configured.
|
|
76
|
+
attr_reader :skill_resource_root
|
|
77
|
+
# Resolver that turns model roles and targets into executable Models.
|
|
78
|
+
attr_reader :model_resolver
|
|
79
|
+
# Shared store used to open per-Run Sessions.
|
|
80
|
+
attr_reader :session_store
|
|
81
|
+
# Configured Workspace implementation, or +nil+ for the default.
|
|
82
|
+
attr_reader :workspace_class
|
|
83
|
+
# Configured Sandbox implementation, or +nil+ for the default.
|
|
84
|
+
attr_reader :sandbox_class
|
|
85
|
+
# Runtime hooks called around request and session preparation.
|
|
86
|
+
attr_reader :runtime_hooks
|
|
46
87
|
|
|
47
88
|
# Starts a runtime from +configuration+ or an existing settings snapshot.
|
|
48
89
|
def initialize(configuration:, settings: nil)
|
|
@@ -129,8 +170,7 @@ module LittleGhost
|
|
|
129
170
|
payload.is_a?(@invocation_class) ? payload : @invocation_class.new(payload)
|
|
130
171
|
end
|
|
131
172
|
|
|
132
|
-
# Creates a Run
|
|
133
|
-
# sandbox resources to it.
|
|
173
|
+
# Creates a Run that owns any workspace and sandbox built for the request.
|
|
134
174
|
def build_run(
|
|
135
175
|
payload,
|
|
136
176
|
agent_class: nil,
|
|
@@ -230,7 +270,7 @@ module LittleGhost
|
|
|
230
270
|
assembly_class_or_name.new(run:, runtime: self)
|
|
231
271
|
end
|
|
232
272
|
|
|
233
|
-
# The
|
|
273
|
+
# The stable service name attached to runtime telemetry.
|
|
234
274
|
def service_name
|
|
235
275
|
@settings&.[](:service_name) || default_service_name
|
|
236
276
|
end
|
|
@@ -264,9 +304,9 @@ module LittleGhost
|
|
|
264
304
|
run
|
|
265
305
|
end
|
|
266
306
|
|
|
267
|
-
def
|
|
307
|
+
def prepare_interjection(run, payload) # :nodoc:
|
|
268
308
|
runtime_hooks.reduce(payload) do |prepared, hook|
|
|
269
|
-
hook.
|
|
309
|
+
hook.prepare_interjection(run, prepared)
|
|
270
310
|
end
|
|
271
311
|
end
|
|
272
312
|
|
data/lib/little_ghost/session.rb
CHANGED
|
@@ -21,7 +21,7 @@ module LittleGhost
|
|
|
21
21
|
# store: session.store
|
|
22
22
|
# )
|
|
23
23
|
# reopened.history.last.text # => "Hello"
|
|
24
|
-
# reopened.state
|
|
24
|
+
# reopened.state[:language] # => "en"
|
|
25
25
|
#
|
|
26
26
|
# === Persistence and trust
|
|
27
27
|
#
|
|
@@ -43,7 +43,7 @@ module LittleGhost
|
|
|
43
43
|
@actor_id = actor_id&.to_s
|
|
44
44
|
@store = store
|
|
45
45
|
@operation_id = operation_id
|
|
46
|
-
@metadata = metadata.
|
|
46
|
+
@metadata = DataMap.new(metadata).freeze
|
|
47
47
|
@loaded = false
|
|
48
48
|
end
|
|
49
49
|
|
|
@@ -63,16 +63,18 @@ module LittleGhost
|
|
|
63
63
|
load&.fetch(:messages) || fallback
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
-
# Exposes a mutable copy of the persisted application state.
|
|
66
|
+
# Exposes a mutable DataMap copy of the persisted application state. String
|
|
67
|
+
# and Symbol keys address the same value; persisted snapshots use Strings.
|
|
67
68
|
def state
|
|
68
69
|
snapshot = load
|
|
69
|
-
snapshot ?
|
|
70
|
+
DataMap.new(snapshot ? snapshot.fetch(:state) : {})
|
|
70
71
|
end
|
|
71
72
|
|
|
72
73
|
# Uses persisted metadata when present and otherwise keeps the metadata from
|
|
73
|
-
# construction.
|
|
74
|
+
# construction. The returned DataMap is frozen.
|
|
74
75
|
def metadata
|
|
75
|
-
|
|
76
|
+
loaded = load
|
|
77
|
+
loaded ? DataMap.new(loaded.fetch(:metadata)).freeze : @metadata
|
|
76
78
|
end
|
|
77
79
|
|
|
78
80
|
# Atomically appends +messages+ when the store still has the expected
|
|
@@ -165,8 +167,8 @@ module LittleGhost
|
|
|
165
167
|
def build_snapshot(messages:, state:, metadata:)
|
|
166
168
|
{
|
|
167
169
|
messages: persistable_messages(messages),
|
|
168
|
-
state:
|
|
169
|
-
metadata:
|
|
170
|
+
state: DataMap.new(state).to_h,
|
|
171
|
+
metadata: DataMap.new(metadata).to_h
|
|
170
172
|
}.freeze
|
|
171
173
|
end
|
|
172
174
|
|
|
@@ -193,8 +195,8 @@ module LittleGhost
|
|
|
193
195
|
|
|
194
196
|
{
|
|
195
197
|
messages: persistable_messages(Array(value.fetch(:messages))),
|
|
196
|
-
state:
|
|
197
|
-
metadata:
|
|
198
|
+
state: DataMap.new(value.fetch(:state, {})).to_h,
|
|
199
|
+
metadata: DataMap.new(value.fetch(:metadata, {})).to_h
|
|
198
200
|
}.freeze
|
|
199
201
|
rescue KeyError, NoMethodError, TypeError => error
|
|
200
202
|
raise ProtocolError, "Session store returned an invalid value: #{error.class}"
|
|
@@ -212,18 +214,5 @@ module LittleGhost
|
|
|
212
214
|
sanitized
|
|
213
215
|
end.freeze
|
|
214
216
|
end
|
|
215
|
-
|
|
216
|
-
def mutable_copy(value)
|
|
217
|
-
case value
|
|
218
|
-
when Hash
|
|
219
|
-
value.to_h { |key, child| [mutable_copy(key), mutable_copy(child)] }
|
|
220
|
-
when Array
|
|
221
|
-
value.map { |child| mutable_copy(child) }
|
|
222
|
-
when String
|
|
223
|
-
value.dup
|
|
224
|
-
else
|
|
225
|
-
value
|
|
226
|
-
end
|
|
227
|
-
end
|
|
228
217
|
end
|
|
229
218
|
end
|
|
@@ -21,9 +21,11 @@ module LittleGhost
|
|
|
21
21
|
# end
|
|
22
22
|
# end
|
|
23
23
|
#
|
|
24
|
-
# A snapshot contains +:messages+, +:state+, and +:metadata+.
|
|
25
|
-
#
|
|
26
|
-
#
|
|
24
|
+
# A snapshot contains +:messages+, +:state+, and +:metadata+. State and
|
|
25
|
+
# metadata cross this boundary as deeply string-keyed JSON mappings. Sessions
|
|
26
|
+
# expose the same data through DataMap, which accepts String or Symbol keys.
|
|
27
|
+
# Implementations provide #load, #append, and #replace; #append must check
|
|
28
|
+
# +expected_count+ atomically so two writers cannot silently lose a turn.
|
|
27
29
|
#
|
|
28
30
|
# Actor identity always comes from the caller. A store must not infer it from
|
|
29
31
|
# ambient process state.
|
|
@@ -39,14 +41,16 @@ module LittleGhost
|
|
|
39
41
|
raise AbstractMethodError, "#{self.class} must implement #load"
|
|
40
42
|
end
|
|
41
43
|
|
|
42
|
-
# Atomically appends sanitized messages and stores state and
|
|
44
|
+
# Atomically appends sanitized messages and stores canonical JSON state and
|
|
45
|
+
# metadata.
|
|
43
46
|
# Implementations raise ProtocolError if the persisted message count differs
|
|
44
47
|
# from +expected_count+.
|
|
45
48
|
def append(_id, messages:, state:, metadata:, expected_count:, actor_id: nil)
|
|
46
49
|
raise AbstractMethodError, "#{self.class} must implement #append"
|
|
47
50
|
end
|
|
48
51
|
|
|
49
|
-
# Replaces the complete snapshot for +id+ atomically
|
|
52
|
+
# Replaces the complete snapshot for +id+ atomically with canonical JSON
|
|
53
|
+
# state and metadata.
|
|
50
54
|
def replace(_id, messages:, state:, metadata:, actor_id: nil)
|
|
51
55
|
raise AbstractMethodError, "#{self.class} must implement #replace"
|
|
52
56
|
end
|