little_ghost 0.2.1 → 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 +72 -74
- data/docs/guides/assemblies.md +286 -0
- data/docs/guides/core_concepts.md +159 -135
- data/docs/guides/getting_started.md +114 -83
- 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 +35 -8
- data/lib/little_ghost/agent/tool_loop.rb +2 -1
- data/lib/little_ghost/agent.rb +280 -326
- data/lib/little_ghost/agent_builder.rb +20 -4
- data/lib/little_ghost/agent_factory.rb +3 -0
- data/lib/little_ghost/{agent_interruptions.rb → agent_interjections.rb} +12 -12
- data/lib/little_ghost/assembly.rb +345 -0
- data/lib/little_ghost/assembly_builder.rb +497 -0
- data/lib/little_ghost/assembly_execution.rb +535 -0
- 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 +10 -2
- data/lib/little_ghost/execution.rb +206 -0
- data/lib/little_ghost/graph.rb +930 -0
- 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 +190 -64
- data/lib/little_ghost/run_context.rb +33 -20
- data/lib/little_ghost/run_result.rb +22 -11
- data/lib/little_ghost/runtime/hook.rb +9 -4
- data/lib/little_ghost/runtime.rb +134 -36
- data/lib/little_ghost/sandbox.rb +1 -1
- 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/support/executor.rb +14 -2
- data/lib/little_ghost/support/loader.rb +2 -2
- data/lib/little_ghost/support.rb +15 -3
- data/lib/little_ghost/swarm.rb +439 -0
- data/lib/little_ghost/tool.rb +88 -20
- data/lib/little_ghost/tools/write_todos.rb +6 -1
- data/lib/little_ghost/tracing/open_telemetry.rb +14 -3
- data/lib/little_ghost/unrestricted_sandbox.rb +1 -1
- data/lib/little_ghost/version.rb +1 -1
- data/lib/little_ghost/workflow.rb +224 -90
- data/lib/little_ghost.rb +36 -25
- metadata +17 -5
data/lib/little_ghost/message.rb
CHANGED
|
@@ -21,8 +21,8 @@ module LittleGhost
|
|
|
21
21
|
attr_reader :role, :content, :metadata
|
|
22
22
|
|
|
23
23
|
# Creates a frozen message with a supported +role+, normalized +content+, and
|
|
24
|
-
# application-defined +metadata+.
|
|
25
|
-
#
|
|
24
|
+
# application-defined +metadata+. Metadata becomes a frozen DataMap, so
|
|
25
|
+
# String and Symbol keys address the same JSON-compatible value.
|
|
26
26
|
def initialize(role:, content:, metadata: {})
|
|
27
27
|
@role = role.to_sym
|
|
28
28
|
raise ArgumentError, "Unsupported message role: #{role.inspect}" unless ROLES.include?(@role)
|
|
@@ -35,7 +35,7 @@ module LittleGhost
|
|
|
35
35
|
[content]
|
|
36
36
|
end
|
|
37
37
|
@content = blocks.map { |block| Content.normalize(block) }.freeze
|
|
38
|
-
@metadata = metadata.freeze
|
|
38
|
+
@metadata = DataMap.new(metadata).freeze
|
|
39
39
|
freeze
|
|
40
40
|
end
|
|
41
41
|
|
|
@@ -64,7 +64,7 @@ module LittleGhost
|
|
|
64
64
|
|
|
65
65
|
# Produces the JSON-safe message representation.
|
|
66
66
|
def to_h
|
|
67
|
-
{"role" => role.to_s, "content" => content.map(&:to_h), "metadata" => metadata}
|
|
67
|
+
{"role" => role.to_s, "content" => content.map(&:to_h), "metadata" => metadata.to_h}
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
# Encodes #to_h as JSON, forwarding generator +arguments+.
|
|
@@ -30,7 +30,7 @@ module LittleGhost
|
|
|
30
30
|
@providers = providers
|
|
31
31
|
@connections = providers&.connections || {}
|
|
32
32
|
@profiles = normalize_profiles(profiles || {})
|
|
33
|
-
@default_model = default_model&.to_s || "default"
|
|
33
|
+
@default_model = (default_model&.to_s || "default").dup.freeze
|
|
34
34
|
@provider_registry = provider_adapters.empty? ? provider_registry : ProviderRegistry.new(adapters: provider_adapters)
|
|
35
35
|
@credential_resolver = credential_resolver
|
|
36
36
|
configure_default_providers unless providers
|
|
@@ -254,7 +254,7 @@ module LittleGhost
|
|
|
254
254
|
|
|
255
255
|
parent = profile["inherits"] || profile[:inherits]
|
|
256
256
|
validate_role_name!(parent.to_s) if parent
|
|
257
|
-
[role, profile.to_h]
|
|
257
|
+
[role, immutable_selection_value(profile.to_h)]
|
|
258
258
|
end
|
|
259
259
|
end
|
|
260
260
|
|
|
@@ -71,6 +71,8 @@ module LittleGhost
|
|
|
71
71
|
#
|
|
72
72
|
# Every configured root is trusted Ruby code because ERB executes inside the
|
|
73
73
|
# current process. Keep roots application-controlled and non-user-writable.
|
|
74
|
+
# See the {Prompts as Views guide}[rdoc-ref:docs/guides/prompt_views.md] for the
|
|
75
|
+
# conventional Agent workflow.
|
|
74
76
|
class PromptResolver
|
|
75
77
|
DEFAULT_MAX_DEPTH = 20 # :nodoc:
|
|
76
78
|
|
data/lib/little_ghost/run.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
|
|
5
5
|
module LittleGhost
|
|
6
|
-
# Observe one top-level
|
|
6
|
+
# Observe one top-level assembly execution from start to finish.
|
|
7
7
|
# A run records its response, outcome, usage, error, and owned resources.
|
|
8
8
|
#
|
|
9
9
|
# run = CustomerSupportAgent.ask("Why is transfer 481 pending?")
|
|
@@ -12,37 +12,81 @@ 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::
|
|
16
|
-
# ask method[rdoc-ref:LittleGhost::
|
|
17
|
-
#
|
|
18
|
-
# helper[rdoc-ref:LittleGhost::
|
|
19
|
-
# method[rdoc-ref:LittleGhost::
|
|
20
|
-
# and returns the same
|
|
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
|
+
# helper[rdoc-ref:LittleGhost::Assembly.stream_ask] or standalone streaming
|
|
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.
|
|
27
36
|
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
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+.
|
|
41
|
+
#
|
|
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
|
-
def initialize(invocation:,
|
|
79
|
+
def initialize(invocation:, runtime:, agent_class: nil, assembly_class: nil, entrypoint_class: nil,
|
|
80
|
+
execution_class: nil,
|
|
42
81
|
cancellation_token: Support::CancellationToken.new, workspace: nil, sandbox: nil)
|
|
82
|
+
entrypoint_class ||= assembly_class || agent_class
|
|
83
|
+
raise ArgumentError, "entrypoint_class is required" unless entrypoint_class
|
|
84
|
+
execution_class ||= assembly_class || entrypoint_class
|
|
85
|
+
|
|
43
86
|
@runtime = runtime
|
|
44
87
|
@agent_class = agent_class
|
|
45
88
|
@entrypoint_class = entrypoint_class
|
|
89
|
+
@execution_class = execution_class
|
|
46
90
|
@invocation = invocation
|
|
47
91
|
@cancellation_token = cancellation_token
|
|
48
92
|
@workspace = workspace
|
|
@@ -55,11 +99,15 @@ module LittleGhost
|
|
|
55
99
|
@event_mutex = Mutex.new
|
|
56
100
|
@subagent_instrumentation_mutex = Mutex.new
|
|
57
101
|
@subagent_instrumentation = {}
|
|
102
|
+
@assembly_step_instrumentation_mutex = Mutex.new
|
|
103
|
+
@assembly_step_instrumentation = {}
|
|
58
104
|
@exclusive_tools_mutex = Mutex.new
|
|
59
105
|
@once_mutex = Mutex.new
|
|
60
106
|
@once_keys = {}
|
|
61
|
-
@
|
|
62
|
-
@
|
|
107
|
+
@interjection_mutex = Mutex.new
|
|
108
|
+
@interjection_condition = ConditionVariable.new
|
|
109
|
+
@interjection_state = :not_started
|
|
110
|
+
@active_interjections = 0
|
|
63
111
|
@entrypoint = nil
|
|
64
112
|
@usage = Usage.new
|
|
65
113
|
end
|
|
@@ -98,40 +146,57 @@ module LittleGhost
|
|
|
98
146
|
# True when cancellation stopped the run without a response.
|
|
99
147
|
def cancelled? = outcome == "cancelled"
|
|
100
148
|
|
|
101
|
-
# Adds an
|
|
149
|
+
# Adds an interjection to the active entrypoint and waits for its response.
|
|
102
150
|
#
|
|
103
|
-
# Raises LittleGhost::
|
|
104
|
-
# after it finishes, or when the entrypoint does not support
|
|
105
|
-
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(
|
|
106
154
|
message,
|
|
107
|
-
|
|
155
|
+
interjection_id: nil,
|
|
108
156
|
batch_key: nil,
|
|
109
157
|
metadata: {},
|
|
110
158
|
cancellation_token: Support::CancellationToken.new,
|
|
111
159
|
deadline: nil
|
|
112
160
|
)
|
|
113
|
-
|
|
114
|
-
|
|
161
|
+
interject_with do
|
|
162
|
+
[
|
|
163
|
+
message,
|
|
164
|
+
{
|
|
165
|
+
interjection_id:,
|
|
166
|
+
batch_key:,
|
|
167
|
+
metadata:,
|
|
168
|
+
cancellation_token:,
|
|
169
|
+
deadline:
|
|
170
|
+
}
|
|
171
|
+
]
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def interject_with # :nodoc:
|
|
176
|
+
entrypoint = @interjection_mutex.synchronize do
|
|
177
|
+
case @interjection_state
|
|
115
178
|
when :not_started, :starting
|
|
116
|
-
raise
|
|
179
|
+
raise AgentInterjectionError, "Run entrypoint is not ready for interjections"
|
|
117
180
|
when :terminal
|
|
118
|
-
raise
|
|
181
|
+
raise AgentInterjectionError, "Run has already finished"
|
|
119
182
|
end
|
|
120
183
|
|
|
184
|
+
@active_interjections += 1
|
|
121
185
|
@entrypoint
|
|
122
186
|
end
|
|
123
|
-
unless entrypoint.
|
|
124
|
-
raise
|
|
187
|
+
unless entrypoint.respond_to?(:interject)
|
|
188
|
+
raise AgentInterjectionError, "Run entrypoint does not support interjections"
|
|
125
189
|
end
|
|
126
190
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
191
|
+
message, options = yield
|
|
192
|
+
entrypoint.interject(message, **options)
|
|
193
|
+
ensure
|
|
194
|
+
if entrypoint
|
|
195
|
+
@interjection_mutex.synchronize do
|
|
196
|
+
@active_interjections -= 1
|
|
197
|
+
@interjection_condition.broadcast
|
|
198
|
+
end
|
|
199
|
+
end
|
|
135
200
|
end
|
|
136
201
|
|
|
137
202
|
# Creates a RunContext with this run's cancellation token and deadline.
|
|
@@ -183,8 +248,8 @@ module LittleGhost
|
|
|
183
248
|
end
|
|
184
249
|
end
|
|
185
250
|
|
|
186
|
-
def
|
|
187
|
-
runtime.
|
|
251
|
+
def prepare_interjection(payload) # :nodoc:
|
|
252
|
+
runtime.prepare_interjection(self, payload)
|
|
188
253
|
end
|
|
189
254
|
|
|
190
255
|
# Closes registered resources in reverse order.
|
|
@@ -209,6 +274,10 @@ module LittleGhost
|
|
|
209
274
|
outcome: cleanup_error ? :error : :cancelled,
|
|
210
275
|
error_type: cleanup_error&.class&.name
|
|
211
276
|
)
|
|
277
|
+
finish_remaining_assembly_step_instrumentation(
|
|
278
|
+
outcome: cleanup_error ? :error : :cancelled,
|
|
279
|
+
error_type: cleanup_error&.class&.name
|
|
280
|
+
)
|
|
212
281
|
rescue => error
|
|
213
282
|
errors << error
|
|
214
283
|
end
|
|
@@ -229,7 +298,7 @@ module LittleGhost
|
|
|
229
298
|
:run,
|
|
230
299
|
parent: nil,
|
|
231
300
|
**correlation_attributes,
|
|
232
|
-
entrypoint_kind
|
|
301
|
+
entrypoint_kind:,
|
|
233
302
|
workflow_name: workflow_run? ? entrypoint_name : nil,
|
|
234
303
|
trace_context: invocation[:parent_trace_context],
|
|
235
304
|
trace_links: invocation[:trace_links],
|
|
@@ -241,17 +310,13 @@ module LittleGhost
|
|
|
241
310
|
workspace&.open(run: self)
|
|
242
311
|
sandbox&.open(run: self)
|
|
243
312
|
@session = runtime.open_session(self)
|
|
244
|
-
agent =
|
|
245
|
-
|
|
246
|
-
else
|
|
247
|
-
entrypoint_class.new(run: self, runtime:)
|
|
248
|
-
end
|
|
249
|
-
@interruption_mutex.synchronize do
|
|
313
|
+
agent = runtime.build_assembly(@execution_class, run: self)
|
|
314
|
+
@interjection_mutex.synchronize do
|
|
250
315
|
@entrypoint = agent
|
|
251
316
|
end
|
|
252
317
|
register(agent)
|
|
253
318
|
invoke = lambda do
|
|
254
|
-
history = session ?
|
|
319
|
+
history = session ? runtime.session_history(self, session, fallback: invocation.history) : invocation.history
|
|
255
320
|
context = session ? session.state.merge(invocation.context) : invocation.context.dup
|
|
256
321
|
options = {
|
|
257
322
|
history:,
|
|
@@ -267,11 +332,11 @@ module LittleGhost
|
|
|
267
332
|
end
|
|
268
333
|
}
|
|
269
334
|
if agent.is_a?(Agent)
|
|
270
|
-
options[:
|
|
271
|
-
@
|
|
335
|
+
options[:interject_ready] = lambda do
|
|
336
|
+
@interjection_mutex.synchronize { @interjection_state = :active }
|
|
272
337
|
end
|
|
273
338
|
else
|
|
274
|
-
@
|
|
339
|
+
@interjection_mutex.synchronize { @interjection_state = :active }
|
|
275
340
|
end
|
|
276
341
|
|
|
277
342
|
agent.stream(invocation.message, **options).each do |event|
|
|
@@ -332,10 +397,11 @@ module LittleGhost
|
|
|
332
397
|
}
|
|
333
398
|
]
|
|
334
399
|
ensure
|
|
335
|
-
@
|
|
400
|
+
@interjection_mutex.synchronize do
|
|
401
|
+
@interjection_state = :terminal
|
|
402
|
+
@interjection_condition.wait(@interjection_mutex) while @active_interjections.positive?
|
|
336
403
|
@last_entrypoint = @entrypoint
|
|
337
404
|
@entrypoint = nil
|
|
338
|
-
@interruption_state = :terminal
|
|
339
405
|
end
|
|
340
406
|
resource_cleanup_error = nil
|
|
341
407
|
begin
|
|
@@ -402,25 +468,39 @@ module LittleGhost
|
|
|
402
468
|
end
|
|
403
469
|
|
|
404
470
|
def correlation_attributes
|
|
405
|
-
{
|
|
471
|
+
attributes = {
|
|
406
472
|
operation_id:,
|
|
407
473
|
run_id: invocation.run_id,
|
|
408
474
|
invocation_id: invocation.invocation_id,
|
|
409
475
|
session_id: invocation.session_id,
|
|
410
|
-
service_name: runtime.service_name
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
476
|
+
service_name: runtime.service_name
|
|
477
|
+
}
|
|
478
|
+
case entrypoint_kind
|
|
479
|
+
when :agent
|
|
480
|
+
attributes[:agent_id] = entrypoint_name
|
|
481
|
+
when :workflow
|
|
482
|
+
attributes[:workflow_name] = entrypoint_name
|
|
483
|
+
else
|
|
484
|
+
attributes[:assembly_id] = entrypoint_name
|
|
485
|
+
attributes[:assembly_kind] = entrypoint_kind
|
|
486
|
+
end
|
|
487
|
+
attributes
|
|
414
488
|
end
|
|
415
489
|
|
|
416
|
-
def workflow_run? = entrypoint_class <= Workflow
|
|
490
|
+
def workflow_run? = defined?(Workflow) && entrypoint_class <= Workflow
|
|
491
|
+
|
|
492
|
+
def entrypoint_kind
|
|
493
|
+
return entrypoint_class.assembly_kind if entrypoint_class.respond_to?(:assembly_kind)
|
|
494
|
+
|
|
495
|
+
workflow_run? ? :workflow : :agent
|
|
496
|
+
end
|
|
417
497
|
|
|
418
498
|
def entrypoint_name
|
|
419
499
|
return entrypoint_class.name.to_s if workflow_run?
|
|
420
500
|
|
|
421
|
-
return @entrypoint.entrypoint_name if @entrypoint
|
|
501
|
+
return @entrypoint.entrypoint_name if @entrypoint&.respond_to?(:entrypoint_name)
|
|
422
502
|
|
|
423
|
-
entrypoint_class.
|
|
503
|
+
entrypoint_class.respond_to?(:assembly_id) ? entrypoint_class.assembly_id : entrypoint_class.name.to_s
|
|
424
504
|
end
|
|
425
505
|
|
|
426
506
|
def instrument_subagent(data)
|
|
@@ -485,7 +565,53 @@ module LittleGhost
|
|
|
485
565
|
error_class ||= error if error.is_a?(String) && error.match?(/\A[A-Z]\w*(?:::[A-Z]\w*)*\z/)
|
|
486
566
|
attributes = data.slice(:attempt, :delay, :error_code, :http_status, :partial_text)
|
|
487
567
|
instrument(:model_retry, attributes.merge(error_class:).compact)
|
|
568
|
+
when :assembly_step_start
|
|
569
|
+
start_assembly_step_instrumentation(data)
|
|
570
|
+
when :assembly_step_stop
|
|
571
|
+
finish_assembly_step_instrumentation(data.fetch(:step_id), data.merge(outcome: :completed))
|
|
572
|
+
when :assembly_step_error
|
|
573
|
+
return unless data[:terminal]
|
|
574
|
+
|
|
575
|
+
finish_assembly_step_instrumentation(data.fetch(:step_id), data.merge(outcome: :error))
|
|
576
|
+
when :assembly_step_retry, :assembly_fork, :assembly_join, :assembly_transition, :assembly_handoff_loop
|
|
577
|
+
instrument(type, data.except(:usage))
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def start_assembly_step_instrumentation(attributes)
|
|
582
|
+
step_id = attributes.fetch(:step_id)
|
|
583
|
+
handle = Instrumentation.start(
|
|
584
|
+
:assembly_step,
|
|
585
|
+
parent: attributes[:parent_operation_id] || operation_id,
|
|
586
|
+
operation_id: step_id,
|
|
587
|
+
detached: true,
|
|
588
|
+
assembly_id: attributes[:assembly_id],
|
|
589
|
+
assembly_kind: attributes[:assembly_kind],
|
|
590
|
+
participant: attributes[:participant],
|
|
591
|
+
branch_id: attributes[:branch_id]
|
|
592
|
+
)
|
|
593
|
+
@assembly_step_instrumentation_mutex.synchronize { @assembly_step_instrumentation[step_id] = handle }
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def finish_assembly_step_instrumentation(step_id, attributes)
|
|
597
|
+
handle = @assembly_step_instrumentation_mutex.synchronize do
|
|
598
|
+
@assembly_step_instrumentation.delete(step_id)
|
|
599
|
+
end
|
|
600
|
+
return unless handle
|
|
601
|
+
|
|
602
|
+
usage = attributes[:usage]
|
|
603
|
+
handle.finish(
|
|
604
|
+
outcome: attributes[:outcome],
|
|
605
|
+
error_type: attributes[:error_type],
|
|
606
|
+
**(usage ? usage_attributes(usage) : {})
|
|
607
|
+
)
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
def finish_remaining_assembly_step_instrumentation(outcome:, error_type: nil)
|
|
611
|
+
handles = @assembly_step_instrumentation_mutex.synchronize do
|
|
612
|
+
@assembly_step_instrumentation.values.tap { @assembly_step_instrumentation.clear }
|
|
488
613
|
end
|
|
614
|
+
handles.reverse_each { |handle| handle.finish(outcome:, error_type:) }
|
|
489
615
|
end
|
|
490
616
|
|
|
491
617
|
def start_subagent_instrumentation(attributes)
|
|
@@ -543,7 +669,7 @@ module LittleGhost
|
|
|
543
669
|
raise Error, "run has already started" if @started
|
|
544
670
|
@started = true
|
|
545
671
|
end
|
|
546
|
-
@
|
|
672
|
+
@interjection_mutex.synchronize { @interjection_state = :starting }
|
|
547
673
|
end
|
|
548
674
|
|
|
549
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
|
|
|
@@ -4,16 +4,16 @@ module LittleGhost
|
|
|
4
4
|
# Associates a validated structured value with its declared schema name.
|
|
5
5
|
StructuredResult = Data.define(:schema_name, :value) # :nodoc:
|
|
6
6
|
|
|
7
|
-
# RunResult gives callers one final view of an
|
|
8
|
-
# It includes the response, usage, updated conversation, state,
|
|
9
|
-
# validated structured value.
|
|
7
|
+
# RunResult gives callers one final view of an assembly invocation.
|
|
8
|
+
# It includes the response, usage, updated conversation, state, coordination
|
|
9
|
+
# steps, and any validated structured value.
|
|
10
10
|
#
|
|
11
11
|
# Use #output when the caller should accept either structured or textual
|
|
12
12
|
# agents. It returns the validated structured value when present and #text
|
|
13
13
|
# otherwise.
|
|
14
|
-
RunResult = Data.define(:message, :stop_reason, :usage, :messages, :state, :structured_result) do # :nodoc:
|
|
15
|
-
def initialize(message:, stop_reason:, usage:, messages:, state:, structured_result: nil)
|
|
16
|
-
super
|
|
14
|
+
RunResult = Data.define(:message, :stop_reason, :usage, :messages, :state, :structured_result, :steps) do # :nodoc:
|
|
15
|
+
def initialize(message:, stop_reason:, usage:, messages:, state:, structured_result: nil, steps: [])
|
|
16
|
+
super(message:, stop_reason:, usage:, messages:, state:, structured_result:, steps: Array(steps).freeze)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
# Reads the final message text, or an empty string when no message exists.
|
|
@@ -30,6 +30,9 @@ module LittleGhost
|
|
|
30
30
|
def output
|
|
31
31
|
structured? ? structured_result.value : text
|
|
32
32
|
end
|
|
33
|
+
|
|
34
|
+
# Returns immutable coordination queries for this result.
|
|
35
|
+
def trajectory = Assembly::Trajectory.new(steps)
|
|
33
36
|
end
|
|
34
37
|
|
|
35
38
|
# Associates a validated structured value with its declared schema name.
|
|
@@ -56,9 +59,9 @@ module LittleGhost
|
|
|
56
59
|
# The locally validated application value.
|
|
57
60
|
end
|
|
58
61
|
|
|
59
|
-
# RunResult gives callers one final view of an
|
|
60
|
-
# It includes the response, usage, updated conversation, state,
|
|
61
|
-
# validated structured value.
|
|
62
|
+
# RunResult gives callers one final view of an Assembly invocation.
|
|
63
|
+
# It includes the response, usage, updated conversation, state, coordination
|
|
64
|
+
# steps, and any validated structured value.
|
|
62
65
|
#
|
|
63
66
|
# Use #output when the caller should accept either structured or textual
|
|
64
67
|
# agents. It returns the validated structured value when present and #text
|
|
@@ -68,9 +71,9 @@ module LittleGhost
|
|
|
68
71
|
# :singleton-method: new
|
|
69
72
|
# :call-seq:
|
|
70
73
|
# new(message:, stop_reason:, usage:, messages:, state:,
|
|
71
|
-
# structured_result: nil) -> RunResult
|
|
74
|
+
# structured_result: nil, steps: []) -> RunResult
|
|
72
75
|
#
|
|
73
|
-
# Creates the terminal value for one
|
|
76
|
+
# Creates the terminal value for one Assembly invocation.
|
|
74
77
|
|
|
75
78
|
##
|
|
76
79
|
# :attr_reader: message
|
|
@@ -96,6 +99,10 @@ module LittleGhost
|
|
|
96
99
|
# :attr_reader: structured_result
|
|
97
100
|
# The validated StructuredResult, or +nil+ for a textual result.
|
|
98
101
|
|
|
102
|
+
##
|
|
103
|
+
# :attr_reader: steps
|
|
104
|
+
# Immutable Assembly::Step records for composite invocations.
|
|
105
|
+
|
|
99
106
|
##
|
|
100
107
|
# :method: text
|
|
101
108
|
# Reads the final message text, or an empty string when no message exists.
|
|
@@ -107,5 +114,9 @@ module LittleGhost
|
|
|
107
114
|
##
|
|
108
115
|
# :method: output
|
|
109
116
|
# Uses the structured value when present and otherwise #text.
|
|
117
|
+
|
|
118
|
+
##
|
|
119
|
+
# :method: trajectory
|
|
120
|
+
# Returns an Assembly::Trajectory for querying immutable coordination steps.
|
|
110
121
|
end
|
|
111
122
|
end
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module LittleGhost
|
|
4
4
|
class Runtime
|
|
5
|
-
# Hooks let applications prepare runs,
|
|
6
|
-
# errors to caller-safe messages.
|
|
5
|
+
# Hooks let applications prepare runs, select session history, transform
|
|
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,13 @@ 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
|
+
|
|
26
|
+
# Returns the history to use for this run, or nil to defer to later hooks
|
|
27
|
+
# and the session default. +stored+ is empty when the session is new;
|
|
28
|
+
# +fallback+ contains messages supplied by the invocation.
|
|
29
|
+
def session_history(_run, stored:, fallback:) = nil
|
|
25
30
|
|
|
26
31
|
# Returns a caller-safe error message, or nil to defer to later hooks and
|
|
27
32
|
# the runtime default. Avoid exposing secrets or internal exception text.
|