little_ghost 0.1.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 +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +122 -0
- data/docs/guides/Core Concepts.md +203 -0
- data/docs/guides/Getting Started.md +187 -0
- data/lib/little_ghost/ag_ui/adapter.rb +194 -0
- data/lib/little_ghost/ag_ui.rb +5 -0
- data/lib/little_ghost/agent/context_management.rb +285 -0
- data/lib/little_ghost/agent/delegation.rb +128 -0
- data/lib/little_ghost/agent/skills.rb +96 -0
- data/lib/little_ghost/agent/tool_loop.rb +239 -0
- data/lib/little_ghost/agent.rb +2111 -0
- data/lib/little_ghost/agent_builder.rb +191 -0
- data/lib/little_ghost/agent_interruptions.rb +197 -0
- data/lib/little_ghost/configuration.rb +337 -0
- data/lib/little_ghost/content.rb +324 -0
- data/lib/little_ghost/default_model_registry.rb +71 -0
- data/lib/little_ghost/errors.rb +48 -0
- data/lib/little_ghost/events.rb +264 -0
- data/lib/little_ghost/execution_state.rb +58 -0
- data/lib/little_ghost/instrumentation.rb +475 -0
- data/lib/little_ghost/invocation.rb +285 -0
- data/lib/little_ghost/lookup.rb +37 -0
- data/lib/little_ghost/mcp/client.rb +396 -0
- data/lib/little_ghost/mcp.rb +5 -0
- data/lib/little_ghost/message.rb +75 -0
- data/lib/little_ghost/model.rb +88 -0
- data/lib/little_ghost/model_capabilities.rb +126 -0
- data/lib/little_ghost/model_registry.rb +173 -0
- data/lib/little_ghost/model_request.rb +107 -0
- data/lib/little_ghost/model_response.rb +48 -0
- data/lib/little_ghost/path_set.rb +32 -0
- data/lib/little_ghost/prompt_resolver.rb +251 -0
- data/lib/little_ghost/providers/bedrock.rb +506 -0
- data/lib/little_ghost/providers/http_transport.rb +149 -0
- data/lib/little_ghost/providers/open_router.rb +171 -0
- data/lib/little_ghost/providers/openai.rb +27 -0
- data/lib/little_ghost/providers/openai_compatible.rb +745 -0
- data/lib/little_ghost/providers/sse_parser.rb +35 -0
- data/lib/little_ghost/run.rb +607 -0
- data/lib/little_ghost/run_context.rb +129 -0
- data/lib/little_ghost/run_result.rb +111 -0
- data/lib/little_ghost/runtime/hook.rb +31 -0
- data/lib/little_ghost/runtime.rb +392 -0
- data/lib/little_ghost/sandbox.rb +138 -0
- data/lib/little_ghost/session.rb +229 -0
- data/lib/little_ghost/session_store.rb +96 -0
- data/lib/little_ghost/session_stores/agent_core_memory.rb +1086 -0
- data/lib/little_ghost/session_stores/memory.rb +86 -0
- data/lib/little_ghost/skills/catalog.rb +283 -0
- data/lib/little_ghost/skills/skill.rb +60 -0
- data/lib/little_ghost/skills.rb +4 -0
- data/lib/little_ghost/stream_event.rb +49 -0
- data/lib/little_ghost/structured_output.rb +126 -0
- data/lib/little_ghost/subagents/agent_path.rb +63 -0
- data/lib/little_ghost/subagents/definition.rb +42 -0
- data/lib/little_ghost/subagents/manager.rb +1615 -0
- data/lib/little_ghost/support/callbacks.rb +151 -0
- data/lib/little_ghost/support/cancellation_token.rb +86 -0
- data/lib/little_ghost/support/class_attributes.rb +40 -0
- data/lib/little_ghost/support/content_capture.rb +150 -0
- data/lib/little_ghost/support/executor.rb +75 -0
- data/lib/little_ghost/support/interruptible_stream.rb +103 -0
- data/lib/little_ghost/support/loader.rb +263 -0
- data/lib/little_ghost/support/output_truncation.rb +71 -0
- data/lib/little_ghost/support/redactor.rb +66 -0
- data/lib/little_ghost/support.rb +34 -0
- data/lib/little_ghost/tool.rb +448 -0
- data/lib/little_ghost/tool_execution.rb +59 -0
- data/lib/little_ghost/tool_registry.rb +156 -0
- data/lib/little_ghost/tools/filesystem.rb +119 -0
- data/lib/little_ghost/tools/shell.rb +45 -0
- data/lib/little_ghost/tools/write_todos.rb +91 -0
- data/lib/little_ghost/tools.rb +6 -0
- data/lib/little_ghost/tracing/open_telemetry.rb +517 -0
- data/lib/little_ghost/unrestricted_sandbox.rb +306 -0
- data/lib/little_ghost/usage.rb +47 -0
- data/lib/little_ghost/version.rb +6 -0
- data/lib/little_ghost/workflow.rb +351 -0
- data/lib/little_ghost/workspace.rb +31 -0
- data/lib/little_ghost.rb +120 -0
- metadata +225 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module LittleGhost
|
|
7
|
+
# Carry one application request into an agent run.
|
|
8
|
+
# An invocation keeps framework fields and application-specific context in one
|
|
9
|
+
# indifferent-key environment.
|
|
10
|
+
#
|
|
11
|
+
# invocation = LittleGhost::Invocation.new(
|
|
12
|
+
# message: "Why is transfer 481 pending?",
|
|
13
|
+
# account_id: "account-1",
|
|
14
|
+
# metadata: {channel: "customer_support"}
|
|
15
|
+
# )
|
|
16
|
+
#
|
|
17
|
+
# invocation.message.text # => "Why is transfer 481 pending?"
|
|
18
|
+
# invocation[:account_id] # => "account-1"
|
|
19
|
+
# invocation.account_id # => "account-1"
|
|
20
|
+
#
|
|
21
|
+
# String and symbol keys address the same field. Known fields have named
|
|
22
|
+
# accessors, while unknown application fields remain available through hash
|
|
23
|
+
# access and dynamic readers or writers. +message+ and every +history+ entry
|
|
24
|
+
# are normalized to Message objects; +deadline_at+ lazily parses ISO 8601 text.
|
|
25
|
+
#
|
|
26
|
+
# Missing run, invocation, and session identifiers are generated when the
|
|
27
|
+
# object is built. Actor identity is never inferred: applications that use it
|
|
28
|
+
# for persistence or tenant isolation must pass a value established by their
|
|
29
|
+
# trusted authentication boundary. Invalid payloads, messages, keys, or
|
|
30
|
+
# deadlines raise InvocationError.
|
|
31
|
+
class Invocation
|
|
32
|
+
DEFAULTS = {
|
|
33
|
+
"history" => -> { [] },
|
|
34
|
+
"settings" => -> { {} },
|
|
35
|
+
"context" => -> { {} },
|
|
36
|
+
"metadata" => -> { {} },
|
|
37
|
+
"model_profiles" => -> { {} }
|
|
38
|
+
}.freeze # :nodoc:
|
|
39
|
+
|
|
40
|
+
ACCESSORS = %i[
|
|
41
|
+
message history settings context metadata model_profiles
|
|
42
|
+
run_id invocation_id session_id actor_id
|
|
43
|
+
].freeze # :nodoc:
|
|
44
|
+
|
|
45
|
+
attr_reader :env # :nodoc:
|
|
46
|
+
|
|
47
|
+
# Copies +env+, normalizes known framework fields, and fills missing identifiers.
|
|
48
|
+
#
|
|
49
|
+
# The payload must be a Hash and must contain a non-blank message.
|
|
50
|
+
def initialize(env = {})
|
|
51
|
+
invalid!("Invocation payload must be an object") unless env.is_a?(Hash)
|
|
52
|
+
|
|
53
|
+
@env = env.to_h { |key, value| [normalize_key(key), duplicate_value(value)] }
|
|
54
|
+
DEFAULTS.each { |key, default| @env[key] = default.call unless @env.key?(key) }
|
|
55
|
+
self.history = history
|
|
56
|
+
self.message = message
|
|
57
|
+
initialize_identifiers!
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
##
|
|
61
|
+
# The normalized current Message.
|
|
62
|
+
# :method: message
|
|
63
|
+
# :call-seq:
|
|
64
|
+
# message() -> LittleGhost::Message
|
|
65
|
+
|
|
66
|
+
##
|
|
67
|
+
# Frozen, normalized Messages that precede the current message.
|
|
68
|
+
# :method: history
|
|
69
|
+
# :call-seq:
|
|
70
|
+
# history() -> Array<LittleGhost::Message>
|
|
71
|
+
|
|
72
|
+
##
|
|
73
|
+
# Per-request model settings merged after profile defaults. Treat these as
|
|
74
|
+
# trusted application policy, not unchecked request or model input.
|
|
75
|
+
# :method: settings
|
|
76
|
+
# :call-seq:
|
|
77
|
+
# settings() -> Hash
|
|
78
|
+
|
|
79
|
+
##
|
|
80
|
+
# JSON-like state made available to the agent run.
|
|
81
|
+
# :method: context
|
|
82
|
+
# :call-seq:
|
|
83
|
+
# context() -> Hash
|
|
84
|
+
|
|
85
|
+
##
|
|
86
|
+
# Application metadata carried with the request.
|
|
87
|
+
# :method: metadata
|
|
88
|
+
# :call-seq:
|
|
89
|
+
# metadata() -> Hash
|
|
90
|
+
|
|
91
|
+
##
|
|
92
|
+
# Per-request overrides for logical model profiles. Overrides can select a
|
|
93
|
+
# provider and model, so applications must construct or allowlist them at a
|
|
94
|
+
# trusted control-plane boundary.
|
|
95
|
+
# :method: model_profiles
|
|
96
|
+
# :call-seq:
|
|
97
|
+
# model_profiles() -> Hash
|
|
98
|
+
|
|
99
|
+
##
|
|
100
|
+
# The caller-supplied or generated top-level run identifier.
|
|
101
|
+
# :method: run_id
|
|
102
|
+
# :call-seq:
|
|
103
|
+
# run_id() -> String
|
|
104
|
+
|
|
105
|
+
##
|
|
106
|
+
# The invocation identifier, defaulting to +run_id+.
|
|
107
|
+
# :method: invocation_id
|
|
108
|
+
# :call-seq:
|
|
109
|
+
# invocation_id() -> String
|
|
110
|
+
|
|
111
|
+
##
|
|
112
|
+
# The session identifier, defaulting to +run_id+.
|
|
113
|
+
# :method: session_id
|
|
114
|
+
# :call-seq:
|
|
115
|
+
# session_id() -> String
|
|
116
|
+
|
|
117
|
+
##
|
|
118
|
+
# The explicit actor identifier supplied by the application.
|
|
119
|
+
# :method: actor_id
|
|
120
|
+
# :call-seq:
|
|
121
|
+
# actor_id() -> value
|
|
122
|
+
|
|
123
|
+
##
|
|
124
|
+
# Replaces the trusted request-scoped model settings.
|
|
125
|
+
# :method: settings=
|
|
126
|
+
# :call-seq:
|
|
127
|
+
# settings=(value) -> value
|
|
128
|
+
|
|
129
|
+
##
|
|
130
|
+
# Replaces the JSON-like agent state.
|
|
131
|
+
# :method: context=
|
|
132
|
+
# :call-seq:
|
|
133
|
+
# context=(value) -> value
|
|
134
|
+
|
|
135
|
+
##
|
|
136
|
+
# Replaces the application metadata.
|
|
137
|
+
# :method: metadata=
|
|
138
|
+
# :call-seq:
|
|
139
|
+
# metadata=(value) -> value
|
|
140
|
+
|
|
141
|
+
##
|
|
142
|
+
# Replaces the trusted per-request model profile overrides.
|
|
143
|
+
# :method: model_profiles=
|
|
144
|
+
# :call-seq:
|
|
145
|
+
# model_profiles=(value) -> value
|
|
146
|
+
|
|
147
|
+
##
|
|
148
|
+
# Replaces the top-level run identifier.
|
|
149
|
+
# :method: run_id=
|
|
150
|
+
# :call-seq:
|
|
151
|
+
# run_id=(value) -> value
|
|
152
|
+
|
|
153
|
+
##
|
|
154
|
+
# Replaces the invocation identifier.
|
|
155
|
+
# :method: invocation_id=
|
|
156
|
+
# :call-seq:
|
|
157
|
+
# invocation_id=(value) -> value
|
|
158
|
+
|
|
159
|
+
##
|
|
160
|
+
# Replaces the session identifier used for persistence.
|
|
161
|
+
# :method: session_id=
|
|
162
|
+
# :call-seq:
|
|
163
|
+
# session_id=(value) -> value
|
|
164
|
+
|
|
165
|
+
##
|
|
166
|
+
# Replaces the application-established actor identifier.
|
|
167
|
+
# :method: actor_id=
|
|
168
|
+
# :call-seq:
|
|
169
|
+
# actor_id=(value) -> value
|
|
170
|
+
ACCESSORS.each do |name|
|
|
171
|
+
define_method(name) { self[name] }
|
|
172
|
+
define_method(:"#{name}=") { |value| self[name] = value } unless %i[message history].include?(name)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Looks up +key+ after normalizing it to a String.
|
|
176
|
+
def [](key) = env[normalize_key(key)]
|
|
177
|
+
|
|
178
|
+
# Stores +value+ under a normalized String key.
|
|
179
|
+
#
|
|
180
|
+
# The +message+ and +history+ fields are normalized before storage.
|
|
181
|
+
def []=(key, value)
|
|
182
|
+
normalized = normalize_key(key)
|
|
183
|
+
value = normalize_message(value) if normalized == "message"
|
|
184
|
+
value = Array(value).map { |message| Message.coerce(message) }.freeze if normalized == "history"
|
|
185
|
+
env[normalized] = value
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Fetches +key+ with the same default and block behavior as Hash#fetch.
|
|
189
|
+
def fetch(key, *defaults, &block) = env.fetch(normalize_key(key), *defaults, &block)
|
|
190
|
+
|
|
191
|
+
# Traverses the environment from normalized +key+ through +names+.
|
|
192
|
+
def dig(key, *names) = env.dig(normalize_key(key), *names)
|
|
193
|
+
|
|
194
|
+
# Whether the environment contains +key+ after normalization.
|
|
195
|
+
def key?(key) = env.key?(normalize_key(key))
|
|
196
|
+
|
|
197
|
+
# Produces a mutable copy of the invocation environment.
|
|
198
|
+
#
|
|
199
|
+
# Nested hashes, arrays, strings, and other duplicable values are copied.
|
|
200
|
+
def to_h = duplicate_value(env)
|
|
201
|
+
|
|
202
|
+
# The request deadline as a Time, parsing ISO 8601 text on first access.
|
|
203
|
+
def deadline_at
|
|
204
|
+
value = self[:deadline_at]
|
|
205
|
+
return value if value.nil? || value.is_a?(Time)
|
|
206
|
+
|
|
207
|
+
self[:deadline_at] = Time.iso8601(String(value))
|
|
208
|
+
rescue ArgumentError, TypeError
|
|
209
|
+
invalid!("deadline_at must be a valid time")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Replaces the deadline; parsing is deferred until +deadline_at+ is read.
|
|
213
|
+
def deadline_at=(value)
|
|
214
|
+
self[:deadline_at] = value
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Replaces and normalizes the current message.
|
|
218
|
+
def message=(value)
|
|
219
|
+
self[:message] = value
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Replaces and freezes the normalized message history.
|
|
223
|
+
def history=(value)
|
|
224
|
+
self[:history] = value
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def method_missing(name, *arguments) # :nodoc:
|
|
228
|
+
value = name.to_s
|
|
229
|
+
if value.end_with?("=") && arguments.length == 1
|
|
230
|
+
return self[value.delete_suffix("=")] = arguments.first
|
|
231
|
+
end
|
|
232
|
+
return self[value] if arguments.empty? && key?(value)
|
|
233
|
+
|
|
234
|
+
super
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def respond_to_missing?(name, include_private = false) # :nodoc:
|
|
238
|
+
value = name.to_s
|
|
239
|
+
value.end_with?("=") || key?(value) || super
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
private
|
|
243
|
+
|
|
244
|
+
def initialize_identifiers!
|
|
245
|
+
self.run_id = generated_id if blank?(run_id)
|
|
246
|
+
self.invocation_id = run_id if blank?(invocation_id)
|
|
247
|
+
self.session_id = run_id if blank?(session_id)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def generated_id = SecureRandom.uuid
|
|
251
|
+
def blank?(value) = value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
252
|
+
|
|
253
|
+
def normalize_key(key)
|
|
254
|
+
key.to_s
|
|
255
|
+
rescue
|
|
256
|
+
invalid!("Invocation keys must be strings or symbols")
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def duplicate_value(value)
|
|
260
|
+
case value
|
|
261
|
+
when Hash then value.to_h { |key, child| [normalize_key(key), duplicate_value(child)] }
|
|
262
|
+
when Array then value.map { |child| duplicate_value(child) }
|
|
263
|
+
when String then value.dup
|
|
264
|
+
else value.dup
|
|
265
|
+
end
|
|
266
|
+
rescue TypeError
|
|
267
|
+
value
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def normalize_message(value)
|
|
271
|
+
invalid!("Invocation requires a message") if blank?(value)
|
|
272
|
+
|
|
273
|
+
return value if value.is_a?(Message)
|
|
274
|
+
return Message.coerce(value) if value.is_a?(Hash)
|
|
275
|
+
|
|
276
|
+
Message.new(role: :user, content: value)
|
|
277
|
+
rescue ArgumentError => error
|
|
278
|
+
invalid!(error.message)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def invalid!(message)
|
|
282
|
+
raise InvocationError, message
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LittleGhost
|
|
4
|
+
# Lookup holds path values shared by prompt and skill discovery.
|
|
5
|
+
module Lookup
|
|
6
|
+
# Root is an expanded lookup +path+ and an optional trusted +boundary+ it must stay
|
|
7
|
+
# within after resolving symbolic links.
|
|
8
|
+
Root = Data.define(:path, :boundary) do # :nodoc:
|
|
9
|
+
def initialize(path:, boundary: nil)
|
|
10
|
+
super(
|
|
11
|
+
path: File.expand_path(path).freeze,
|
|
12
|
+
boundary: boundary && File.expand_path(boundary).freeze
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Holds an expanded lookup path and the optional trusted boundary it must
|
|
18
|
+
# remain within after symbolic links are resolved.
|
|
19
|
+
class Root < Data # :doc:
|
|
20
|
+
##
|
|
21
|
+
# :singleton-method: new
|
|
22
|
+
# :call-seq:
|
|
23
|
+
# new(path:, boundary: nil) -> Root
|
|
24
|
+
#
|
|
25
|
+
# Expands +path+ and the optional +boundary+ without resolving symbolic
|
|
26
|
+
# links.
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# :attr_reader: path
|
|
30
|
+
# The expanded lookup path.
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# :attr_reader: boundary
|
|
34
|
+
# The expanded containment boundary, or +nil+ when none was supplied.
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|