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.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +22 -0
  3. data/README.md +122 -0
  4. data/docs/guides/Core Concepts.md +203 -0
  5. data/docs/guides/Getting Started.md +187 -0
  6. data/lib/little_ghost/ag_ui/adapter.rb +194 -0
  7. data/lib/little_ghost/ag_ui.rb +5 -0
  8. data/lib/little_ghost/agent/context_management.rb +285 -0
  9. data/lib/little_ghost/agent/delegation.rb +128 -0
  10. data/lib/little_ghost/agent/skills.rb +96 -0
  11. data/lib/little_ghost/agent/tool_loop.rb +239 -0
  12. data/lib/little_ghost/agent.rb +2111 -0
  13. data/lib/little_ghost/agent_builder.rb +191 -0
  14. data/lib/little_ghost/agent_interruptions.rb +197 -0
  15. data/lib/little_ghost/configuration.rb +337 -0
  16. data/lib/little_ghost/content.rb +324 -0
  17. data/lib/little_ghost/default_model_registry.rb +71 -0
  18. data/lib/little_ghost/errors.rb +48 -0
  19. data/lib/little_ghost/events.rb +264 -0
  20. data/lib/little_ghost/execution_state.rb +58 -0
  21. data/lib/little_ghost/instrumentation.rb +475 -0
  22. data/lib/little_ghost/invocation.rb +285 -0
  23. data/lib/little_ghost/lookup.rb +37 -0
  24. data/lib/little_ghost/mcp/client.rb +396 -0
  25. data/lib/little_ghost/mcp.rb +5 -0
  26. data/lib/little_ghost/message.rb +75 -0
  27. data/lib/little_ghost/model.rb +88 -0
  28. data/lib/little_ghost/model_capabilities.rb +126 -0
  29. data/lib/little_ghost/model_registry.rb +173 -0
  30. data/lib/little_ghost/model_request.rb +107 -0
  31. data/lib/little_ghost/model_response.rb +48 -0
  32. data/lib/little_ghost/path_set.rb +32 -0
  33. data/lib/little_ghost/prompt_resolver.rb +251 -0
  34. data/lib/little_ghost/providers/bedrock.rb +506 -0
  35. data/lib/little_ghost/providers/http_transport.rb +149 -0
  36. data/lib/little_ghost/providers/open_router.rb +171 -0
  37. data/lib/little_ghost/providers/openai.rb +27 -0
  38. data/lib/little_ghost/providers/openai_compatible.rb +745 -0
  39. data/lib/little_ghost/providers/sse_parser.rb +35 -0
  40. data/lib/little_ghost/run.rb +607 -0
  41. data/lib/little_ghost/run_context.rb +129 -0
  42. data/lib/little_ghost/run_result.rb +111 -0
  43. data/lib/little_ghost/runtime/hook.rb +31 -0
  44. data/lib/little_ghost/runtime.rb +392 -0
  45. data/lib/little_ghost/sandbox.rb +138 -0
  46. data/lib/little_ghost/session.rb +229 -0
  47. data/lib/little_ghost/session_store.rb +96 -0
  48. data/lib/little_ghost/session_stores/agent_core_memory.rb +1086 -0
  49. data/lib/little_ghost/session_stores/memory.rb +86 -0
  50. data/lib/little_ghost/skills/catalog.rb +283 -0
  51. data/lib/little_ghost/skills/skill.rb +60 -0
  52. data/lib/little_ghost/skills.rb +4 -0
  53. data/lib/little_ghost/stream_event.rb +49 -0
  54. data/lib/little_ghost/structured_output.rb +126 -0
  55. data/lib/little_ghost/subagents/agent_path.rb +63 -0
  56. data/lib/little_ghost/subagents/definition.rb +42 -0
  57. data/lib/little_ghost/subagents/manager.rb +1615 -0
  58. data/lib/little_ghost/support/callbacks.rb +151 -0
  59. data/lib/little_ghost/support/cancellation_token.rb +86 -0
  60. data/lib/little_ghost/support/class_attributes.rb +40 -0
  61. data/lib/little_ghost/support/content_capture.rb +150 -0
  62. data/lib/little_ghost/support/executor.rb +75 -0
  63. data/lib/little_ghost/support/interruptible_stream.rb +103 -0
  64. data/lib/little_ghost/support/loader.rb +263 -0
  65. data/lib/little_ghost/support/output_truncation.rb +71 -0
  66. data/lib/little_ghost/support/redactor.rb +66 -0
  67. data/lib/little_ghost/support.rb +34 -0
  68. data/lib/little_ghost/tool.rb +448 -0
  69. data/lib/little_ghost/tool_execution.rb +59 -0
  70. data/lib/little_ghost/tool_registry.rb +156 -0
  71. data/lib/little_ghost/tools/filesystem.rb +119 -0
  72. data/lib/little_ghost/tools/shell.rb +45 -0
  73. data/lib/little_ghost/tools/write_todos.rb +91 -0
  74. data/lib/little_ghost/tools.rb +6 -0
  75. data/lib/little_ghost/tracing/open_telemetry.rb +517 -0
  76. data/lib/little_ghost/unrestricted_sandbox.rb +306 -0
  77. data/lib/little_ghost/usage.rb +47 -0
  78. data/lib/little_ghost/version.rb +6 -0
  79. data/lib/little_ghost/workflow.rb +351 -0
  80. data/lib/little_ghost/workspace.rb +31 -0
  81. data/lib/little_ghost.rb +120 -0
  82. 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