little_ghost 0.3.0 → 0.5.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 +90 -84
- data/docs/guides/assemblies.md +412 -0
- data/docs/guides/code_mode.md +275 -0
- data/docs/guides/core_concepts.md +150 -239
- data/docs/guides/getting_started.md +125 -87
- data/docs/guides/integrations.md +217 -0
- data/docs/guides/models_and_providers.md +125 -0
- data/docs/guides/production.md +253 -0
- data/docs/guides/prompt_views.md +139 -0
- data/docs/guides/sandboxing.md +282 -0
- data/docs/guides/skills.md +141 -0
- data/docs/guides/structured_outputs_and_content.md +135 -0
- data/docs/guides/tools.md +329 -0
- data/lib/little_ghost/ag_ui/adapter.rb +5 -5
- data/lib/little_ghost/agent/delegation.rb +3 -3
- data/lib/little_ghost/agent/skills.rb +6 -1
- data/lib/little_ghost/agent/tool_loop.rb +6 -2
- data/lib/little_ghost/agent.rb +411 -214
- data/lib/little_ghost/agent_builder.rb +61 -22
- data/lib/little_ghost/{agent_interruptions.rb → agent_interjections.rb} +12 -12
- data/lib/little_ghost/agent_stream_source.rb +262 -0
- data/lib/little_ghost/artifact.rb +182 -0
- data/lib/little_ghost/artifacts/presentation_budget.rb +44 -0
- data/lib/little_ghost/artifacts/workspace_store.rb +370 -0
- data/lib/little_ghost/artifacts.rb +11 -0
- data/lib/little_ghost/assembly.rb +110 -30
- data/lib/little_ghost/assembly_builder.rb +59 -21
- data/lib/little_ghost/assembly_execution.rb +110 -6
- data/lib/little_ghost/code_mode/broker.rb +164 -0
- data/lib/little_ghost/code_mode/catalog.rb +54 -0
- data/lib/little_ghost/code_mode/engine.rb +58 -0
- data/lib/little_ghost/code_mode/javascript/catalog.rb +118 -0
- data/lib/little_ghost/code_mode/javascript/client.rb +550 -0
- data/lib/little_ghost/code_mode/javascript/host.rb +347 -0
- data/lib/little_ghost/code_mode/javascript/session.rb +579 -0
- data/lib/little_ghost/code_mode/javascript_engine.rb +172 -0
- data/lib/little_ghost/code_mode/protocol.rb +78 -0
- data/lib/little_ghost/code_mode/ruby/catalog.rb +80 -0
- data/lib/little_ghost/code_mode/ruby/host.rb +102 -0
- data/lib/little_ghost/code_mode/ruby/session.rb +508 -0
- data/lib/little_ghost/code_mode/ruby_engine.rb +86 -0
- data/lib/little_ghost/code_mode/runtime.rb +335 -0
- data/lib/little_ghost/code_mode/session.rb +61 -0
- data/lib/little_ghost/code_mode/types.rb +58 -0
- data/lib/little_ghost/code_mode.rb +53 -0
- data/lib/little_ghost/configuration.rb +366 -47
- data/lib/little_ghost/content.rb +24 -13
- data/lib/little_ghost/data_map.rb +209 -0
- data/lib/little_ghost/errors.rb +28 -9
- data/lib/little_ghost/execution.rb +33 -34
- data/lib/little_ghost/graph.rb +376 -197
- data/lib/little_ghost/mcp/client.rb +487 -88
- data/lib/little_ghost/mcp/toolset.rb +210 -0
- data/lib/little_ghost/mcp/types.rb +216 -0
- data/lib/little_ghost/mcp.rb +3 -0
- data/lib/little_ghost/message.rb +4 -4
- data/lib/little_ghost/model_capabilities.rb +9 -6
- data/lib/little_ghost/model_request.rb +0 -12
- data/lib/little_ghost/model_resolver.rb +15 -5
- data/lib/little_ghost/model_response.rb +3 -7
- data/lib/little_ghost/network/authorizer_server.rb +162 -0
- data/lib/little_ghost/network/certificate_authority.rb +98 -0
- data/lib/little_ghost/network/envoy_config.rb +362 -0
- data/lib/little_ghost/network/envoy_gateway.rb +409 -0
- data/lib/little_ghost/network/external_gateway.rb +68 -0
- data/lib/little_ghost/network.rb +96 -0
- data/lib/little_ghost/prompt_resolver.rb +9 -7
- data/lib/little_ghost/provider_registry.rb +3 -3
- data/lib/little_ghost/providers/anthropic.rb +8 -1
- data/lib/little_ghost/providers/gemini.rb +10 -1
- data/lib/little_ghost/providers/vertex_ai.rb +6 -1
- data/lib/little_ghost/run.rb +162 -54
- data/lib/little_ghost/run_context.rb +42 -20
- data/lib/little_ghost/run_result.rb +0 -7
- data/lib/little_ghost/runtime/hook.rb +8 -3
- data/lib/little_ghost/runtime/hooks/artifacts.rb +338 -0
- data/lib/little_ghost/runtime.rb +160 -52
- data/lib/little_ghost/sandbox/capabilities.rb +68 -0
- data/lib/little_ghost/sandbox/environment_policy.rb +41 -0
- data/lib/little_ghost/sandbox/filesystem.rb +377 -0
- data/lib/little_ghost/sandbox/isolated_backend.rb +163 -0
- data/lib/little_ghost/sandbox/limits.rb +48 -0
- data/lib/little_ghost/sandbox/mount.rb +129 -0
- data/lib/little_ghost/sandbox/network_policy.rb +108 -0
- data/lib/little_ghost/sandbox/policy.rb +88 -0
- data/lib/little_ghost/sandbox/process_runner.rb +103 -0
- data/lib/little_ghost/sandbox/process_session.rb +335 -0
- data/lib/little_ghost/sandbox/scope.rb +304 -0
- data/lib/little_ghost/sandbox.rb +158 -32
- data/lib/little_ghost/sandboxes/bubblewrap.rb +351 -0
- data/lib/little_ghost/sandboxes/native.rb +51 -0
- data/lib/little_ghost/sandboxes/seatbelt.rb +261 -0
- data/lib/little_ghost/sandboxes/unrestricted.rb +241 -0
- data/lib/little_ghost/session.rb +39 -26
- 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/skills/catalog.rb +94 -14
- data/lib/little_ghost/skills/resource_root.rb +42 -0
- data/lib/little_ghost/skills/skill.rb +0 -3
- data/lib/little_ghost/stream_event.rb +8 -13
- data/lib/little_ghost/subagents/control_tool.rb +8 -0
- data/lib/little_ghost/subagents/manager.rb +53 -50
- data/lib/little_ghost/support/callbacks.rb +3 -1
- data/lib/little_ghost/support/content_capture.rb +3 -3
- data/lib/little_ghost/support/http_client.rb +2 -2
- data/lib/little_ghost/support/redactor.rb +1 -1
- data/lib/little_ghost/swarm.rb +13 -5
- data/lib/little_ghost/tool.rb +157 -64
- data/lib/little_ghost/tool_registry.rb +1 -1
- data/lib/little_ghost/tools/filesystem.rb +1 -1
- data/lib/little_ghost/tools/shell.rb +4 -3
- 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/workspace.rb +222 -8
- data/lib/little_ghost.rb +40 -27
- metadata +104 -3
- data/lib/little_ghost/unrestricted_sandbox.rb +0 -306
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LittleGhost
|
|
4
|
+
module MCP
|
|
5
|
+
# Connects one MCP server to an Agent as a reusable Tool provider. Each Agent
|
|
6
|
+
# run gets its own connection. +map_tool+ chooses and configures the generated
|
|
7
|
+
# Tool classes; +map_result+ converts results that need application-specific
|
|
8
|
+
# handling.
|
|
9
|
+
#
|
|
10
|
+
# class HelpCenterTools < LittleGhost::MCP::Toolset
|
|
11
|
+
# connection url: "https://mcp.example/rpc", timeout: 20
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# class CustomerSupportAgent < LittleGhost::Agent
|
|
15
|
+
# tools HelpCenterTools
|
|
16
|
+
# end
|
|
17
|
+
class Toolset
|
|
18
|
+
extend Support::ClassAttributes
|
|
19
|
+
|
|
20
|
+
CONNECTION_OPTIONS = %i[url headers timeout signer allow_insecure_http max_response_bytes].freeze # :nodoc:
|
|
21
|
+
UNSET = Object.new.freeze # :nodoc:
|
|
22
|
+
|
|
23
|
+
class CallbackFailure < Error # :nodoc:
|
|
24
|
+
attr_reader :original
|
|
25
|
+
|
|
26
|
+
def initialize(original)
|
|
27
|
+
@original = original
|
|
28
|
+
super(original.message)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class_attribute :connection_value
|
|
33
|
+
class_attribute :tool_mapping_value
|
|
34
|
+
class_attribute :result_mapping_value
|
|
35
|
+
class_attribute :optional_value, default: false
|
|
36
|
+
class_attribute :error_callback_value
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
# Declares a static connection Hash or a block called with the current
|
|
40
|
+
# Tool::Binding. The Hash requires +url+ and may include
|
|
41
|
+
# +headers+, +timeout+, +signer+, +allow_insecure_http+, and
|
|
42
|
+
# +max_response_bytes+.
|
|
43
|
+
#
|
|
44
|
+
# :call-seq:
|
|
45
|
+
# connection() -> Hash, Proc, nil
|
|
46
|
+
# connection(values) -> Hash
|
|
47
|
+
# connection { |binding| ... } -> Proc
|
|
48
|
+
def connection(value = UNSET, &resolver)
|
|
49
|
+
return connection_value if value.equal?(UNSET) && !resolver
|
|
50
|
+
raise ArgumentError, "provide a connection or a block, not both" unless value.equal?(UNSET) || !resolver
|
|
51
|
+
|
|
52
|
+
configured = resolver || value
|
|
53
|
+
unless configured.is_a?(Hash) || configured.respond_to?(:call)
|
|
54
|
+
raise ArgumentError, "connection must be a hash or callable"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
self.connection_value = configured.is_a?(Hash) ? normalize_connection(configured) : configured
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Maps each generated Tool class. The block receives +definition:+ and
|
|
61
|
+
# +binding:+ keywords. Return the configured Tool class, or nil to omit
|
|
62
|
+
# it. Changing Tool#tool_name does not change the operation name sent to
|
|
63
|
+
# the MCP server.
|
|
64
|
+
#
|
|
65
|
+
# :call-seq:
|
|
66
|
+
# map_tool() -> Proc, nil
|
|
67
|
+
# map_tool { |tool_class, definition:, binding:| ... } -> Proc
|
|
68
|
+
def map_tool(&mapping)
|
|
69
|
+
return tool_mapping_value unless mapping
|
|
70
|
+
|
|
71
|
+
self.tool_mapping_value = mapping
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Maps each immutable MCP::Result. The block receives +call:+ and
|
|
75
|
+
# +binding:+ keywords and returns any Ruby value or Tool::Result.
|
|
76
|
+
#
|
|
77
|
+
# :call-seq:
|
|
78
|
+
# map_result() -> Proc, nil
|
|
79
|
+
# map_result { |result, call:, binding:| ... } -> Proc
|
|
80
|
+
def map_result(&mapping)
|
|
81
|
+
return result_mapping_value unless mapping
|
|
82
|
+
|
|
83
|
+
self.result_mapping_value = mapping
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Makes expected provider and protocol discovery failures produce no
|
|
87
|
+
# tools. Configuration, cancellation, deadline, and callback failures
|
|
88
|
+
# still propagate.
|
|
89
|
+
#
|
|
90
|
+
# :call-seq:
|
|
91
|
+
# optional() -> true or false
|
|
92
|
+
# optional(value) -> true or false
|
|
93
|
+
def optional(value = UNSET)
|
|
94
|
+
return optional_value if value.equal?(UNSET)
|
|
95
|
+
|
|
96
|
+
self.optional_value = !!value
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Observes an expected discovery failure caught by <tt>optional true</tt>.
|
|
100
|
+
# Exceptions raised by this callback propagate.
|
|
101
|
+
#
|
|
102
|
+
# :call-seq:
|
|
103
|
+
# on_error() -> Proc, nil
|
|
104
|
+
# on_error { |error, binding:| ... } -> Proc
|
|
105
|
+
def on_error(&callback)
|
|
106
|
+
return error_callback_value unless callback
|
|
107
|
+
|
|
108
|
+
self.error_callback_value = callback
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Generates Tool classes for an Agent's current binding.
|
|
112
|
+
def tools(binding)
|
|
113
|
+
options = resolved_connection(binding)
|
|
114
|
+
context = binding.run.context if binding.run&.respond_to?(:context)
|
|
115
|
+
mapper = tool_mapping_value
|
|
116
|
+
result_mapper = result_mapping_value
|
|
117
|
+
options = connection_with_wrapped_signer(options)
|
|
118
|
+
Instrumentation.instrument(:mcp_discovery, toolset: toolset_name) do |telemetry|
|
|
119
|
+
client = Client.new(
|
|
120
|
+
transport: HTTPTransport.new(**options),
|
|
121
|
+
name: toolset_name,
|
|
122
|
+
tool_mapper: mapper && lambda do |tool_class, definition:, binding:|
|
|
123
|
+
invoke_application_callback do
|
|
124
|
+
mapper.call(tool_class, definition:, binding:)
|
|
125
|
+
end
|
|
126
|
+
end,
|
|
127
|
+
result_mapper: result_mapper && lambda do |result, call:, binding:|
|
|
128
|
+
result_mapper.call(result, call:, binding:)
|
|
129
|
+
end
|
|
130
|
+
)
|
|
131
|
+
discovered = client.tools(context:, binding:)
|
|
132
|
+
telemetry[:outcome] = :success
|
|
133
|
+
telemetry[:tool_count] = discovered.length
|
|
134
|
+
discovered
|
|
135
|
+
end
|
|
136
|
+
rescue CallbackFailure => error
|
|
137
|
+
raise error.original
|
|
138
|
+
rescue ProviderError, ProtocolError, ToolError => error
|
|
139
|
+
raise unless optional_value
|
|
140
|
+
|
|
141
|
+
error_callback_value&.call(error, binding:)
|
|
142
|
+
[]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
private
|
|
146
|
+
|
|
147
|
+
def resolved_connection(binding)
|
|
148
|
+
configured = connection_value
|
|
149
|
+
raise ConfigurationError, "#{toolset_name} must declare an MCP connection" unless configured
|
|
150
|
+
|
|
151
|
+
value = if configured.respond_to?(:call)
|
|
152
|
+
invoke_application_callback { configured.call(binding) }
|
|
153
|
+
else
|
|
154
|
+
configured
|
|
155
|
+
end
|
|
156
|
+
normalize_connection(value)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def normalize_connection(value)
|
|
160
|
+
hash = Hash.try_convert(value)
|
|
161
|
+
raise ConfigurationError, "#{toolset_name} connection must resolve to a hash" unless hash
|
|
162
|
+
|
|
163
|
+
normalized = hash.each_with_object({}) do |(name, child), result|
|
|
164
|
+
key = name.to_sym
|
|
165
|
+
raise ConfigurationError, "#{toolset_name} connection contains duplicate #{key}" if result.key?(key)
|
|
166
|
+
|
|
167
|
+
result[key] = child
|
|
168
|
+
end
|
|
169
|
+
unknown = normalized.keys - CONNECTION_OPTIONS
|
|
170
|
+
unless unknown.empty?
|
|
171
|
+
raise ConfigurationError, "#{toolset_name} connection contains unknown option #{unknown.first.inspect}"
|
|
172
|
+
end
|
|
173
|
+
url = normalized[:url]
|
|
174
|
+
raise ConfigurationError, "#{toolset_name} connection must include a URL" if url.nil? || url.to_s.empty?
|
|
175
|
+
|
|
176
|
+
normalized[:url] = String(url).freeze
|
|
177
|
+
normalized[:headers] = normalize_headers(normalized.fetch(:headers, {}))
|
|
178
|
+
normalized.freeze
|
|
179
|
+
rescue NoMethodError
|
|
180
|
+
raise ConfigurationError, "#{toolset_name} connection keys must be strings or symbols"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def normalize_headers(headers)
|
|
184
|
+
headers.each_with_object({}) do |(name, value), normalized|
|
|
185
|
+
normalized[String(name).dup.freeze] = String(value).dup.freeze
|
|
186
|
+
end.freeze
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def connection_with_wrapped_signer(options)
|
|
190
|
+
signer = options[:signer]
|
|
191
|
+
return options unless signer
|
|
192
|
+
|
|
193
|
+
options.merge(
|
|
194
|
+
signer: ->(request) { invoke_application_callback { signer.call(request) } }
|
|
195
|
+
).freeze
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def invoke_application_callback
|
|
199
|
+
yield
|
|
200
|
+
rescue ProviderError, ProtocolError, ToolError => error
|
|
201
|
+
raise CallbackFailure.new(error)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def toolset_name
|
|
205
|
+
name || "MCP Toolset"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LittleGhost
|
|
4
|
+
module MCP
|
|
5
|
+
module ImmutableValue # :nodoc:
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def mapping(value, field:)
|
|
9
|
+
raise ProtocolError, "MCP #{field} must be an object" unless value.is_a?(Hash)
|
|
10
|
+
|
|
11
|
+
freeze_value(DataMap.new(value))
|
|
12
|
+
rescue ArgumentError => error
|
|
13
|
+
raise ProtocolError, "MCP #{field} is invalid: #{error.message}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def optional_mapping(value, field:)
|
|
17
|
+
return if value.nil?
|
|
18
|
+
|
|
19
|
+
mapping(value, field:)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def array(value, field:)
|
|
23
|
+
raise ProtocolError, "MCP #{field} must be an array" unless value.is_a?(Array)
|
|
24
|
+
|
|
25
|
+
freeze_value(DataMap.new("items" => value).fetch("items"))
|
|
26
|
+
rescue ArgumentError => error
|
|
27
|
+
raise ProtocolError, "MCP #{field} is invalid: #{error.message}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def freeze_value(value)
|
|
31
|
+
pending = [value]
|
|
32
|
+
until pending.empty?
|
|
33
|
+
current = pending.pop
|
|
34
|
+
case current
|
|
35
|
+
when Hash
|
|
36
|
+
current.each do |key, child|
|
|
37
|
+
key.freeze
|
|
38
|
+
pending << child
|
|
39
|
+
end
|
|
40
|
+
when Array
|
|
41
|
+
current.each { |child| pending << child }
|
|
42
|
+
end
|
|
43
|
+
current.freeze
|
|
44
|
+
end
|
|
45
|
+
value
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
Definition = Data.define( # :nodoc:
|
|
50
|
+
:source_name,
|
|
51
|
+
:name,
|
|
52
|
+
:description,
|
|
53
|
+
:input_schema,
|
|
54
|
+
:output_schema,
|
|
55
|
+
:annotations,
|
|
56
|
+
:title,
|
|
57
|
+
:metadata,
|
|
58
|
+
:raw
|
|
59
|
+
) do
|
|
60
|
+
def initialize(source_name:, name:, description:, input_schema:, raw:, output_schema: nil,
|
|
61
|
+
annotations: {}, title: nil, metadata: {})
|
|
62
|
+
source_name = String(source_name)
|
|
63
|
+
name = String(name)
|
|
64
|
+
raise ProtocolError, "MCP tool definition must include a name" if source_name.empty?
|
|
65
|
+
raise ConfigurationError, "MCP tool name cannot be empty" if name.empty?
|
|
66
|
+
|
|
67
|
+
super(
|
|
68
|
+
source_name: source_name.freeze,
|
|
69
|
+
name: name.freeze,
|
|
70
|
+
description: String(description).freeze,
|
|
71
|
+
input_schema: ImmutableValue.mapping(input_schema, field: "tool inputSchema"),
|
|
72
|
+
output_schema: ImmutableValue.optional_mapping(output_schema, field: "tool outputSchema"),
|
|
73
|
+
annotations: ImmutableValue.mapping(annotations, field: "tool annotations"),
|
|
74
|
+
title: title.nil? ? nil : String(title).freeze,
|
|
75
|
+
metadata: ImmutableValue.mapping(metadata, field: "tool _meta"),
|
|
76
|
+
raw: ImmutableValue.mapping(raw, field: "tool definition")
|
|
77
|
+
)
|
|
78
|
+
rescue TypeError
|
|
79
|
+
raise ProtocolError, "MCP tool definition is invalid"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def [](key) = raw[key]
|
|
83
|
+
def fetch(...) = raw.fetch(...)
|
|
84
|
+
def dig(...) = raw.dig(...)
|
|
85
|
+
def key?(key) = raw.key?(key)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Immutable server-advertised Tool metadata. +source_name+ is always the
|
|
89
|
+
# name sent back to the server; +name+ is the initially generated
|
|
90
|
+
# model-facing name and may later be customized on the generated Tool class.
|
|
91
|
+
class Definition < Data # :doc:
|
|
92
|
+
##
|
|
93
|
+
# :attr_reader: source_name
|
|
94
|
+
# The unmodified server-advertised name used for dispatch.
|
|
95
|
+
|
|
96
|
+
##
|
|
97
|
+
# :attr_reader: name
|
|
98
|
+
# The normalized, optionally prefixed initial model-facing name.
|
|
99
|
+
|
|
100
|
+
##
|
|
101
|
+
# :attr_reader: description
|
|
102
|
+
# The model-facing description, including LittleGhost's fallback when the
|
|
103
|
+
# server omitted one.
|
|
104
|
+
|
|
105
|
+
##
|
|
106
|
+
# :attr_reader: input_schema
|
|
107
|
+
# The deeply frozen MCP input schema.
|
|
108
|
+
|
|
109
|
+
##
|
|
110
|
+
# :attr_reader: output_schema
|
|
111
|
+
# The optional deeply frozen MCP output schema.
|
|
112
|
+
|
|
113
|
+
##
|
|
114
|
+
# :attr_reader: annotations
|
|
115
|
+
# Deeply frozen MCP tool annotations.
|
|
116
|
+
|
|
117
|
+
##
|
|
118
|
+
# :attr_reader: title
|
|
119
|
+
# The optional human-readable MCP title.
|
|
120
|
+
|
|
121
|
+
##
|
|
122
|
+
# :attr_reader: metadata
|
|
123
|
+
# Deeply frozen MCP <tt>_meta</tt> object.
|
|
124
|
+
|
|
125
|
+
##
|
|
126
|
+
# :attr_reader: raw
|
|
127
|
+
# A deeply frozen, indifferent-access copy of the complete server
|
|
128
|
+
# definition object.
|
|
129
|
+
# Definition also delegates +[]+, +fetch+, +dig+, and +key?+ to this map.
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
Call = Data.define(:definition, :arguments, :context, :binding) do # :nodoc:
|
|
133
|
+
def initialize(definition:, arguments:, context: nil, binding: Tool::Binding.new)
|
|
134
|
+
raise ArgumentError, "definition must be a LittleGhost::MCP::Definition" unless definition.is_a?(Definition)
|
|
135
|
+
|
|
136
|
+
super(
|
|
137
|
+
definition:,
|
|
138
|
+
arguments: ImmutableValue.mapping(arguments, field: "tool call arguments"),
|
|
139
|
+
context:,
|
|
140
|
+
binding:
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Describes one MCP Tool invocation. It combines the server's Definition,
|
|
146
|
+
# the copied arguments, and the Tool::Binding used to execute the generated
|
|
147
|
+
# Tool.
|
|
148
|
+
class Call < Data # :doc:
|
|
149
|
+
##
|
|
150
|
+
# :attr_reader: definition
|
|
151
|
+
# The Definition whose +source_name+ is sent to the server.
|
|
152
|
+
|
|
153
|
+
##
|
|
154
|
+
# :attr_reader: arguments
|
|
155
|
+
# Deeply frozen, indifferent-access arguments.
|
|
156
|
+
|
|
157
|
+
##
|
|
158
|
+
# :attr_reader: context
|
|
159
|
+
# The RunContext controlling cancellation, deadlines, and working state.
|
|
160
|
+
|
|
161
|
+
##
|
|
162
|
+
# :attr_reader: binding
|
|
163
|
+
# The Tool::Binding for the generated Tool instance.
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
Result = Data.define(:content, :structured_content, :error, :metadata, :raw) do # :nodoc:
|
|
167
|
+
def initialize(content:, raw:, structured_content: nil, error: false, metadata: {})
|
|
168
|
+
unless error.nil? || error == true || error == false
|
|
169
|
+
raise ProtocolError, "MCP tool result isError must be boolean"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
super(
|
|
173
|
+
content: ImmutableValue.array(content, field: "tool result content"),
|
|
174
|
+
structured_content: ImmutableValue.optional_mapping(
|
|
175
|
+
structured_content,
|
|
176
|
+
field: "structuredContent"
|
|
177
|
+
),
|
|
178
|
+
error: !!error,
|
|
179
|
+
metadata: ImmutableValue.mapping(metadata, field: "tool result _meta"),
|
|
180
|
+
raw: ImmutableValue.mapping(raw, field: "tool result")
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def error? = error
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Represents one MCP Tool result without discarding server fields. Mapping
|
|
188
|
+
# callbacks can inspect normalized content, structured content, metadata, or
|
|
189
|
+
# the complete protocol value.
|
|
190
|
+
class Result < Data # :doc:
|
|
191
|
+
##
|
|
192
|
+
# :attr_reader: content
|
|
193
|
+
# Deeply frozen MCP content blocks.
|
|
194
|
+
|
|
195
|
+
##
|
|
196
|
+
# :attr_reader: structured_content
|
|
197
|
+
# The optional deeply frozen structured result object.
|
|
198
|
+
|
|
199
|
+
##
|
|
200
|
+
# :attr_reader: error
|
|
201
|
+
# Whether the server marked the result with <tt>isError</tt>.
|
|
202
|
+
|
|
203
|
+
##
|
|
204
|
+
# :attr_reader: metadata
|
|
205
|
+
# Deeply frozen MCP result <tt>_meta</tt>.
|
|
206
|
+
|
|
207
|
+
##
|
|
208
|
+
# :attr_reader: raw
|
|
209
|
+
# A deeply frozen copy of the complete server result object.
|
|
210
|
+
|
|
211
|
+
##
|
|
212
|
+
# :method: error?
|
|
213
|
+
# Whether the server marked the result as an error.
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
data/lib/little_ghost/mcp.rb
CHANGED
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+.
|
|
@@ -5,9 +5,10 @@ module LittleGhost
|
|
|
5
5
|
# It keeps structured results and tool selection from relying on provider
|
|
6
6
|
# guesswork.
|
|
7
7
|
#
|
|
8
|
-
# A +nil+ +supported_parameters+ list means parameter
|
|
9
|
-
# Use +ModelCapabilities.unknown+ when
|
|
10
|
-
#
|
|
8
|
+
# A +nil+ +supported_parameters+ list means the parameter list is not
|
|
9
|
+
# restricted. Use +ModelCapabilities.unknown+ when metadata is unavailable.
|
|
10
|
+
# Callers must check +known?+ before treating +supports_parameter?+ as evidence
|
|
11
|
+
# that a provider supports a feature.
|
|
11
12
|
ModelCapabilities = Data.define( # :nodoc:
|
|
12
13
|
:native_structured_output,
|
|
13
14
|
:tools,
|
|
@@ -59,7 +60,8 @@ module LittleGhost
|
|
|
59
60
|
|
|
60
61
|
# Describes the optional features a model can use. Providers expose this value
|
|
61
62
|
# so structured results and tool selection do not rely on provider guesswork.
|
|
62
|
-
# A +nil+ supported-parameter list means
|
|
63
|
+
# A +nil+ supported-parameter list means the list itself is unrestricted.
|
|
64
|
+
# Check +known?+ before treating that as known provider support.
|
|
63
65
|
class ModelCapabilities < Data # :doc:
|
|
64
66
|
##
|
|
65
67
|
# :singleton-method: new
|
|
@@ -112,8 +114,9 @@ module LittleGhost
|
|
|
112
114
|
# :call-seq:
|
|
113
115
|
# supports_parameter?(*names) -> boolean
|
|
114
116
|
#
|
|
115
|
-
# Checks
|
|
116
|
-
#
|
|
117
|
+
# Checks only the parameter-list restriction. It returns true for every name
|
|
118
|
+
# when the list is +nil+, including on an unknown capability value. Check
|
|
119
|
+
# +known?+ when the caller needs evidence of provider support.
|
|
117
120
|
|
|
118
121
|
##
|
|
119
122
|
# :singleton-method: permissive
|
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module LittleGhost
|
|
4
|
-
# ModelRequest carries everything a provider needs for one model stream. It
|
|
5
|
-
# keeps messages, tools, settings, structured-output requirements, and
|
|
6
|
-
# cooperative execution controls together.
|
|
7
|
-
#
|
|
8
|
-
# Messages are coerced into Message objects, and required capabilities are
|
|
9
|
-
# normalized to unique symbols. The messages, tools, settings, and capability
|
|
10
|
-
# containers are frozen; output schema and tool choice values are retained as
|
|
11
|
-
# supplied.
|
|
12
|
-
#
|
|
13
|
-
# Only those outer containers are frozen. Do not mutate retained output
|
|
14
|
-
# schemas, tool choices, or nested settings after construction, and do not
|
|
15
|
-
# share mutable control values across concurrent requests.
|
|
16
4
|
ModelRequest = Data.define( # :nodoc:
|
|
17
5
|
:messages,
|
|
18
6
|
:tools,
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module LittleGhost
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
4
|
+
# Maps an application model role to an executable Model.
|
|
5
|
+
#
|
|
6
|
+
# resolver = LittleGhost::ModelResolver.new(
|
|
7
|
+
# profiles: {
|
|
8
|
+
# customer_support: {target: "openrouter:openai/gpt-5.6-luna"}
|
|
9
|
+
# }
|
|
10
|
+
# )
|
|
11
|
+
# # With OPENROUTER_API_KEY set:
|
|
12
|
+
# model = resolver.resolve(:customer_support)
|
|
13
|
+
#
|
|
14
|
+
# A role is an application-facing name such as +:customer_support+. Callers
|
|
15
|
+
# may also pass a +provider:model-id+ target or an inline configuration
|
|
16
|
+
# mapping.
|
|
7
17
|
class ModelResolver
|
|
8
18
|
DEFAULT_CREDENTIALS = [ # :nodoc:
|
|
9
19
|
["LITTLEGHOST_OPENROUTER_API_KEY", "openrouter"],
|
|
@@ -30,7 +40,7 @@ module LittleGhost
|
|
|
30
40
|
@providers = providers
|
|
31
41
|
@connections = providers&.connections || {}
|
|
32
42
|
@profiles = normalize_profiles(profiles || {})
|
|
33
|
-
@default_model = default_model&.to_s || "default"
|
|
43
|
+
@default_model = (default_model&.to_s || "default").dup.freeze
|
|
34
44
|
@provider_registry = provider_adapters.empty? ? provider_registry : ProviderRegistry.new(adapters: provider_adapters)
|
|
35
45
|
@credential_resolver = credential_resolver
|
|
36
46
|
configure_default_providers unless providers
|
|
@@ -254,7 +264,7 @@ module LittleGhost
|
|
|
254
264
|
|
|
255
265
|
parent = profile["inherits"] || profile[:inherits]
|
|
256
266
|
validate_role_name!(parent.to_s) if parent
|
|
257
|
-
[role, profile.to_h]
|
|
267
|
+
[role, immutable_selection_value(profile.to_h)]
|
|
258
268
|
end
|
|
259
269
|
end
|
|
260
270
|
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module LittleGhost
|
|
4
|
-
# ModelResponse is the final result shared by every provider stream. It keeps
|
|
5
|
-
# provider-specific response shapes out of the agent loop.
|
|
6
|
-
#
|
|
7
|
-
# +message+ is the assistant Message, +stop_reason+ is a normalized symbol,
|
|
8
|
-
# and +usage+ contains provider-independent token counts.
|
|
9
4
|
ModelResponse = Data.define(:message, :stop_reason, :usage, :metadata) do # :nodoc:
|
|
10
5
|
# Creates an immutable response and coerces +message+ into a Message.
|
|
11
6
|
def initialize(message:, stop_reason:, usage: Usage.new, metadata: {})
|
|
@@ -27,7 +22,7 @@ module LittleGhost
|
|
|
27
22
|
# new(message:, stop_reason:, usage: Usage.new, metadata: {}) -> ModelResponse
|
|
28
23
|
#
|
|
29
24
|
# Coerces +message+ to Message, normalizes +stop_reason+ to a Symbol, and
|
|
30
|
-
# freezes +metadata
|
|
25
|
+
# freezes the outer +metadata+ Hash.
|
|
31
26
|
|
|
32
27
|
##
|
|
33
28
|
# :attr_reader: message
|
|
@@ -43,6 +38,7 @@ module LittleGhost
|
|
|
43
38
|
|
|
44
39
|
##
|
|
45
40
|
# :attr_reader: metadata
|
|
46
|
-
#
|
|
41
|
+
# Provider-specific response metadata with a frozen outer Hash. Nested
|
|
42
|
+
# values are retained and must not be mutated by callers.
|
|
47
43
|
end
|
|
48
44
|
end
|