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,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "openai_compatible"
4
+
5
+ module LittleGhost
6
+ module Providers
7
+ # OpenRouter gives one LittleGhost provider access to models routed through
8
+ # OpenRouter. Agents keep their model roles while the registry selects an
9
+ # OpenRouter model for each role.
10
+ #
11
+ # provider = LittleGhost::Providers::OpenRouter.new(
12
+ # api_key: ENV.fetch("OPENROUTER_API_KEY"),
13
+ # model: ENV.fetch("OPENROUTER_MODEL"),
14
+ # app_name: "Support Console"
15
+ # )
16
+ #
17
+ # +site_url+ and +app_name+ populate attribution headers. Capability metadata
18
+ # controls structured output, tools, tool choice, and parameter filtering.
19
+ # Requests that need a capability ask OpenRouter to route only to providers
20
+ # that advertise it.
21
+ class OpenRouter < OpenAICompatible
22
+ # The OpenRouter API endpoint used when +base_url+ is omitted.
23
+ DEFAULT_BASE_URL = "https://openrouter.ai/api/v1/"
24
+ TOP_LEVEL_CACHE_MODELS = ["anthropic/", "~anthropic/"].freeze # :nodoc:
25
+ MESSAGE_CACHE_MODELS = [
26
+ "google/gemini",
27
+ "qwen/qwen3-max",
28
+ "qwen/qwen-plus",
29
+ "qwen/qwen3.6-plus",
30
+ "qwen/qwen3-coder-plus",
31
+ "qwen/qwen3-coder-flash",
32
+ "deepseek/deepseek-v3.2"
33
+ ].freeze # :nodoc:
34
+
35
+ # Configures an OpenRouter client. All OpenAICompatible options, including
36
+ # +api_key+, +model+, retry limits, timeouts, and custom transport, apply.
37
+ def initialize(site_url: nil, app_name: nil, base_url: DEFAULT_BASE_URL, **arguments)
38
+ @site_url = site_url
39
+ @app_name = app_name
40
+ super(base_url:, api: :chat_completions, **arguments)
41
+ end
42
+
43
+ # Reads model capabilities from OpenRouter +supported_parameters+ metadata.
44
+ # Missing metadata produces ModelCapabilities.unknown.
45
+ def capabilities(metadata: {})
46
+ parameters = metadata[:supported_parameters] || metadata["supported_parameters"]
47
+ return ModelCapabilities.unknown unless parameters.is_a?(Array)
48
+
49
+ values = parameters.map(&:to_s)
50
+ ModelCapabilities.new(
51
+ native_structured_output: values.any? { |value| %w[structured_outputs response_format].include?(value) },
52
+ tools: values.include?("tools"),
53
+ tool_choice: values.include?("tool_choice"),
54
+ supported_parameters: values
55
+ )
56
+ end
57
+
58
+ # Filters unsupported model settings when the request requires advertised
59
+ # capabilities.
60
+ def prepare_request(request, capabilities:)
61
+ return request if request.required_capabilities.empty?
62
+ return request unless capabilities.supported_parameters
63
+
64
+ settings = filter_supported_settings(request.settings, capabilities)
65
+ ModelRequest.new(
66
+ messages: request.messages,
67
+ tools: request.tools,
68
+ settings:,
69
+ output_schema: request.output_schema,
70
+ tool_choice: request.tool_choice,
71
+ required_capabilities: request.required_capabilities,
72
+ cancellation_token: request.cancellation_token,
73
+ deadline: request.deadline
74
+ )
75
+ end
76
+
77
+ private
78
+
79
+ MODEL_SETTING_PARAMETERS = { # :nodoc:
80
+ temperature: %w[temperature],
81
+ top_p: %w[top_p],
82
+ max_tokens: %w[max_tokens max_completion_tokens],
83
+ max_completion_tokens: %w[max_completion_tokens max_tokens],
84
+ stop: %w[stop],
85
+ seed: %w[seed],
86
+ reasoning: %w[reasoning reasoning_effort],
87
+ reasoning_effort: %w[reasoning reasoning_effort]
88
+ }.freeze
89
+
90
+ def filter_supported_settings(settings, capabilities)
91
+ result = settings.to_h.dup
92
+ MODEL_SETTING_PARAMETERS.each do |setting, parameters|
93
+ key = result.key?(setting) ? setting : setting.to_s
94
+ next unless result.key?(key)
95
+
96
+ result.delete(key) unless capabilities.supports_parameter?(*parameters)
97
+ end
98
+ max_tokens_key = result.key?(:max_tokens) ? :max_tokens : "max_tokens"
99
+ if result.key?(max_tokens_key) &&
100
+ !capabilities.supports_parameter?("max_tokens") &&
101
+ capabilities.supports_parameter?("max_completion_tokens")
102
+ result[result.key?(:max_tokens) ? :max_completion_tokens : "max_completion_tokens"] =
103
+ result.delete(max_tokens_key)
104
+ end
105
+ result.freeze
106
+ end
107
+
108
+ def dynamic_headers(request)
109
+ session_id = request.settings[:session_id] || request.settings["session_id"]
110
+ compact_hash(
111
+ "HTTP-Referer" => @site_url,
112
+ "X-Title" => @app_name,
113
+ "x-session-id" => session_id&.to_s&.slice(0, 256)
114
+ )
115
+ end
116
+
117
+ def provider_settings(settings)
118
+ result = super
119
+ reasoning = settings[:reasoning] || settings["reasoning"]
120
+ effort = settings[:reasoning_effort] || settings["reasoning_effort"]
121
+ result[:reasoning] = reasoning || {effort:} if reasoning || effort
122
+ session_id = settings[:session_id] || settings["session_id"]
123
+ result[:session_id] = session_id.to_s.slice(0, 256) if session_id
124
+ result[:cache_control] = {type: "ephemeral"} if model_prefix?(TOP_LEVEL_CACHE_MODELS)
125
+ prompt_cache_key = settings[:prompt_cache_key] || settings["prompt_cache_key"]
126
+ result[:prompt_cache_key] = prompt_cache_key if prompt_cache_key
127
+ result
128
+ end
129
+
130
+ def chat_reasoning_fields(message)
131
+ return {} unless message.role == :assistant
132
+
133
+ blocks = message.content.grep(Content::Reasoning)
134
+ text = blocks.map(&:text).reject(&:empty?).join
135
+ details = blocks.flat_map { |block| Array(block.details) }
136
+ compact_hash(
137
+ reasoning: text.empty? ? nil : text,
138
+ reasoning_details: details.empty? ? nil : details
139
+ )
140
+ end
141
+
142
+ def request_body(request)
143
+ body = super
144
+ if request.output_schema || !request.required_capabilities.empty?
145
+ body[:provider] = {require_parameters: true}
146
+ end
147
+ add_message_cache_control(body[:messages]) if model_prefix?(MESSAGE_CACHE_MODELS)
148
+ body
149
+ end
150
+
151
+ def model_prefix?(prefixes)
152
+ normalized = model.downcase
153
+ prefixes.any? { |prefix| normalized.start_with?(prefix) }
154
+ end
155
+
156
+ def add_message_cache_control(messages)
157
+ message = messages.find { |candidate| %w[system developer].include?(candidate[:role]) }
158
+ return unless message
159
+
160
+ content = message[:content]
161
+ if content.is_a?(String)
162
+ message[:content] = [{type: "text", text: content, cache_control: {type: "ephemeral"}}]
163
+ return
164
+ end
165
+
166
+ block = Array(content).reverse.find { |candidate| candidate[:type] == "text" && !candidate.key?(:cache_control) }
167
+ block[:cache_control] = {type: "ephemeral"} if block
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "openai_compatible"
4
+
5
+ module LittleGhost
6
+ module Providers
7
+ # OpenAI connects LittleGhost agents to OpenAI models with streaming, tools,
8
+ # and structured results. It uses the Responses API by default.
9
+ #
10
+ # provider = LittleGhost::Providers::OpenAI.new(
11
+ # api_key: ENV.fetch("OPENAI_API_KEY"),
12
+ # model: ENV.fetch("OPENAI_MODEL")
13
+ # )
14
+ #
15
+ # Supply <tt>api: :chat_completions</tt> only when a model or integration requires
16
+ # the Chat Completions wire API.
17
+ class OpenAI < OpenAICompatible
18
+ # The OpenAI API endpoint used when +base_url+ is omitted.
19
+ DEFAULT_BASE_URL = "https://api.openai.com/v1/"
20
+
21
+ # Uses the official OpenAI API base URL by default.
22
+ def initialize(base_url: DEFAULT_BASE_URL, **arguments)
23
+ super
24
+ end
25
+ end
26
+ end
27
+ end