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
|
@@ -29,17 +29,18 @@ module LittleGhost
|
|
|
29
29
|
model: nil,
|
|
30
30
|
tools: [],
|
|
31
31
|
conversation_id: nil,
|
|
32
|
-
agent_path: Subagents::AgentPath::ROOT
|
|
32
|
+
agent_path: Subagents::AgentPath::ROOT,
|
|
33
|
+
agent_stream_path: []
|
|
33
34
|
)
|
|
34
35
|
agent_class = @resolve_agent.call(agent_class_or_name)
|
|
35
|
-
build_agent(agent_class, run:, model:, tools:, conversation_id:, agent_path:)
|
|
36
|
+
build_agent(agent_class, run:, model:, tools:, conversation_id:, agent_path:, agent_stream_path:)
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
private
|
|
39
40
|
|
|
40
41
|
attr_reader :runtime, :prompt_paths
|
|
41
42
|
|
|
42
|
-
def instantiate(agent_class, run:, tools:, model:, delegation_activity:, agent_path:)
|
|
43
|
+
def instantiate(agent_class, run:, tools:, model:, delegation_activity:, agent_path:, agent_stream_path:)
|
|
43
44
|
agent_class.new(
|
|
44
45
|
model:,
|
|
45
46
|
runtime:,
|
|
@@ -51,7 +52,7 @@ module LittleGhost
|
|
|
51
52
|
delegation_activity:,
|
|
52
53
|
agent_path:,
|
|
53
54
|
**agent_class.limits
|
|
54
|
-
)
|
|
55
|
+
).bind_agent_stream_path(agent_stream_path)
|
|
55
56
|
end
|
|
56
57
|
|
|
57
58
|
def build_agent(
|
|
@@ -60,12 +61,13 @@ module LittleGhost
|
|
|
60
61
|
model:,
|
|
61
62
|
tools:,
|
|
62
63
|
conversation_id: nil,
|
|
63
|
-
agent_path: Subagents::AgentPath::ROOT
|
|
64
|
+
agent_path: Subagents::AgentPath::ROOT,
|
|
65
|
+
agent_stream_path: []
|
|
64
66
|
)
|
|
65
67
|
delegation_activity = ActivityRelay.new
|
|
66
68
|
configured_tools = Array(tools).dup
|
|
67
69
|
configured_tools.concat(
|
|
68
|
-
delegation_tools(agent_class, run, conversation_id:, delegation_activity:, agent_path:)
|
|
70
|
+
delegation_tools(agent_class, run, conversation_id:, delegation_activity:, agent_path:, agent_stream_path:)
|
|
69
71
|
)
|
|
70
72
|
resolved_model = model || runtime.model_for(agent_class, run)
|
|
71
73
|
transferred = true
|
|
@@ -75,25 +77,33 @@ module LittleGhost
|
|
|
75
77
|
model: resolved_model,
|
|
76
78
|
tools: configured_tools,
|
|
77
79
|
delegation_activity:,
|
|
78
|
-
agent_path
|
|
80
|
+
agent_path:,
|
|
81
|
+
agent_stream_path:
|
|
79
82
|
)
|
|
80
83
|
rescue
|
|
81
84
|
close_tools(configured_tools) unless transferred
|
|
82
85
|
raise
|
|
83
86
|
end
|
|
84
87
|
|
|
85
|
-
def delegation_tools(agent_class, run, conversation_id:, delegation_activity:, agent_path:)
|
|
86
|
-
tools = agent_tools(agent_class, run)
|
|
87
|
-
tools.concat(subagent_tools(
|
|
88
|
+
def delegation_tools(agent_class, run, conversation_id:, delegation_activity:, agent_path:, agent_stream_path:)
|
|
89
|
+
tools = agent_tools(agent_class, run, agent_stream_path:)
|
|
90
|
+
tools.concat(subagent_tools(
|
|
91
|
+
agent_class,
|
|
92
|
+
run,
|
|
93
|
+
conversation_id:,
|
|
94
|
+
delegation_activity:,
|
|
95
|
+
agent_path:,
|
|
96
|
+
agent_stream_path:
|
|
97
|
+
))
|
|
88
98
|
rescue
|
|
89
99
|
close_tools(tools)
|
|
90
100
|
raise
|
|
91
101
|
end
|
|
92
102
|
|
|
93
|
-
def agent_tools(agent_class, run)
|
|
103
|
+
def agent_tools(agent_class, run, agent_stream_path:)
|
|
94
104
|
tools = []
|
|
95
105
|
agent_class.assembly_tool_declarations.each do |declaration|
|
|
96
|
-
child = declared_assembly(declaration, run)
|
|
106
|
+
child = declared_assembly(declaration, run, agent_stream_path:)
|
|
97
107
|
begin
|
|
98
108
|
tools << child.as_tool(
|
|
99
109
|
name: declaration.fetch(:name),
|
|
@@ -111,18 +121,18 @@ module LittleGhost
|
|
|
111
121
|
raise
|
|
112
122
|
end
|
|
113
123
|
|
|
114
|
-
def declared_assembly(declaration, run)
|
|
124
|
+
def declared_assembly(declaration, run, agent_stream_path:)
|
|
115
125
|
assembly_class = declaration.fetch(:assembly)
|
|
116
126
|
if assembly_class.is_a?(AssemblyDefinition) && assembly_class.kind == :agent
|
|
117
|
-
declared_agent(declaration.merge(agent: assembly_class.implementation), run)
|
|
127
|
+
declared_agent(declaration.merge(agent: assembly_class.implementation), run, agent_stream_path:)
|
|
118
128
|
elsif assembly_class <= Agent
|
|
119
|
-
declared_agent(declaration.merge(agent: assembly_class), run)
|
|
129
|
+
declared_agent(declaration.merge(agent: assembly_class), run, agent_stream_path:)
|
|
120
130
|
else
|
|
121
|
-
runtime.build_assembly(assembly_class, run:)
|
|
131
|
+
runtime.build_assembly(assembly_class, run:, agent_stream_path:)
|
|
122
132
|
end
|
|
123
133
|
end
|
|
124
134
|
|
|
125
|
-
def subagent_tools(agent_class, run, conversation_id:, delegation_activity:, agent_path:)
|
|
135
|
+
def subagent_tools(agent_class, run, conversation_id:, delegation_activity:, agent_path:, agent_stream_path:)
|
|
126
136
|
definitions = agent_class.subagent_declarations.map do |declaration|
|
|
127
137
|
Subagents::Definition.new(
|
|
128
138
|
kind: declaration.fetch(:kind),
|
|
@@ -132,13 +142,16 @@ module LittleGhost
|
|
|
132
142
|
factory: lambda do |subagent_id, child_conversation_id = nil|
|
|
133
143
|
factory = declaration[:factory]
|
|
134
144
|
if factory
|
|
135
|
-
invoke_factory(factory, subagent_id, run)
|
|
145
|
+
invoke_factory(factory, subagent_id, run).tap do |agent|
|
|
146
|
+
agent.bind_agent_stream_path(agent_stream_path) if agent.respond_to?(:bind_agent_stream_path)
|
|
147
|
+
end
|
|
136
148
|
else
|
|
137
149
|
declared_agent(
|
|
138
150
|
declaration,
|
|
139
151
|
run,
|
|
140
152
|
conversation_id: child_conversation_id,
|
|
141
|
-
agent_path: subagent_id
|
|
153
|
+
agent_path: subagent_id,
|
|
154
|
+
agent_stream_path:
|
|
142
155
|
)
|
|
143
156
|
end
|
|
144
157
|
end
|
|
@@ -150,7 +163,11 @@ module LittleGhost
|
|
|
150
163
|
unless resolved.all? { |definition| definition.is_a?(Subagents::Definition) }
|
|
151
164
|
raise ConfigurationError, "Subagent resolvers must return LittleGhost::Subagents::Definition objects"
|
|
152
165
|
end
|
|
153
|
-
definitions.concat(
|
|
166
|
+
definitions.concat(
|
|
167
|
+
resolved
|
|
168
|
+
.reject { |definition| declared_kinds.include?(definition.kind) }
|
|
169
|
+
.map { |definition| stream_bound_definition(definition, agent_stream_path) }
|
|
170
|
+
)
|
|
154
171
|
end
|
|
155
172
|
return [] if definitions.empty?
|
|
156
173
|
|
|
@@ -169,7 +186,13 @@ module LittleGhost
|
|
|
169
186
|
).tools
|
|
170
187
|
end
|
|
171
188
|
|
|
172
|
-
def declared_agent(
|
|
189
|
+
def declared_agent(
|
|
190
|
+
declaration,
|
|
191
|
+
run,
|
|
192
|
+
conversation_id: nil,
|
|
193
|
+
agent_path: Subagents::AgentPath::ROOT,
|
|
194
|
+
agent_stream_path: []
|
|
195
|
+
)
|
|
173
196
|
reference = declaration.fetch(:agent)
|
|
174
197
|
agent_class = if reference.is_a?(AssemblyDefinition)
|
|
175
198
|
reference.implementation
|
|
@@ -181,7 +204,23 @@ module LittleGhost
|
|
|
181
204
|
model: resolve(declaration[:model], run),
|
|
182
205
|
tools: Array(resolve(declaration[:tools], run)),
|
|
183
206
|
conversation_id:,
|
|
184
|
-
agent_path
|
|
207
|
+
agent_path:,
|
|
208
|
+
agent_stream_path:
|
|
209
|
+
)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def stream_bound_definition(definition, agent_stream_path)
|
|
213
|
+
factory = definition.factory
|
|
214
|
+
Subagents::Definition.new(
|
|
215
|
+
kind: definition.kind,
|
|
216
|
+
description: definition.description,
|
|
217
|
+
persist: definition.persist,
|
|
218
|
+
accepts_conversation_id: definition.accepts_conversation_id,
|
|
219
|
+
factory: lambda do |*arguments, **_options|
|
|
220
|
+
invoke_factory(factory, *arguments).tap do |agent|
|
|
221
|
+
agent.bind_agent_stream_path(agent_stream_path) if agent.respond_to?(:bind_agent_stream_path)
|
|
222
|
+
end
|
|
223
|
+
end
|
|
185
224
|
)
|
|
186
225
|
end
|
|
187
226
|
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
require "securerandom"
|
|
4
4
|
|
|
5
5
|
module LittleGhost
|
|
6
|
-
class
|
|
6
|
+
class AgentInterjections # :nodoc: all
|
|
7
7
|
MAX_BATCH_SIZE = 100
|
|
8
|
-
|
|
8
|
+
MAX_INTERJECTION_COUNT = 1_000
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
def initialize(text:, tool_calls:,
|
|
10
|
+
Result = Data.define(:text, :tool_calls, :interjection_ids, :batch_key) do
|
|
11
|
+
def initialize(text:, tool_calls:, interjection_ids: [], batch_key: nil)
|
|
12
12
|
super(
|
|
13
13
|
text: String(text),
|
|
14
14
|
tool_calls: !!tool_calls,
|
|
15
|
-
|
|
15
|
+
interjection_ids: Array(interjection_ids).map { |id| String(id).dup.freeze }.freeze,
|
|
16
16
|
batch_key: batch_key.nil? ? nil : String(batch_key).dup.freeze
|
|
17
17
|
)
|
|
18
18
|
end
|
|
@@ -21,7 +21,7 @@ module LittleGhost
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
Batch = Data.define(:tickets) do
|
|
24
|
-
def
|
|
24
|
+
def interjection_ids = tickets.map(&:id)
|
|
25
25
|
def batch_key = tickets.first&.batch_key
|
|
26
26
|
def metadata = tickets.last&.metadata
|
|
27
27
|
end
|
|
@@ -99,7 +99,7 @@ module LittleGhost
|
|
|
99
99
|
|
|
100
100
|
def enqueue(message, id: SecureRandom.uuid, batch_key: nil, metadata: {})
|
|
101
101
|
id = String(id)
|
|
102
|
-
raise ArgumentError, "
|
|
102
|
+
raise ArgumentError, "interjection_id cannot be empty" if id.empty?
|
|
103
103
|
|
|
104
104
|
batch_key = String(batch_key) unless batch_key.nil?
|
|
105
105
|
metadata = metadata.to_h
|
|
@@ -111,14 +111,14 @@ module LittleGhost
|
|
|
111
111
|
unless existing.message.to_h == message.to_h &&
|
|
112
112
|
existing.batch_key == batch_key &&
|
|
113
113
|
existing.metadata == metadata
|
|
114
|
-
raise ArgumentError, "
|
|
114
|
+
raise ArgumentError, "interjection_id has already been used with different input"
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
@waiters[existing] += 1
|
|
118
118
|
return existing
|
|
119
119
|
end
|
|
120
|
-
if @tickets_by_id.length >=
|
|
121
|
-
raise
|
|
120
|
+
if @tickets_by_id.length >= MAX_INTERJECTION_COUNT
|
|
121
|
+
raise AgentInterjectionError, "Agent interjection capacity reached"
|
|
122
122
|
end
|
|
123
123
|
|
|
124
124
|
ticket = Ticket.new(message, id: id.freeze, batch_key: batch_key&.freeze, metadata:)
|
|
@@ -128,7 +128,7 @@ module LittleGhost
|
|
|
128
128
|
ticket
|
|
129
129
|
end
|
|
130
130
|
rescue TypeError, NoMethodError
|
|
131
|
-
raise ArgumentError, "
|
|
131
|
+
raise ArgumentError, "interjection_id, batch_key, and metadata are invalid"
|
|
132
132
|
end
|
|
133
133
|
|
|
134
134
|
def deliver
|
|
@@ -166,7 +166,7 @@ module LittleGhost
|
|
|
166
166
|
return true if @closed_error
|
|
167
167
|
return false if @delivered || !@queue.empty?
|
|
168
168
|
|
|
169
|
-
@closed_error =
|
|
169
|
+
@closed_error = AgentInterjectionError.new("Agent is not currently running")
|
|
170
170
|
true
|
|
171
171
|
end
|
|
172
172
|
end
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LittleGhost
|
|
4
|
+
AgentStreamStep = Data.define( # :nodoc:
|
|
5
|
+
:assembly_id,
|
|
6
|
+
:assembly_kind,
|
|
7
|
+
:participant,
|
|
8
|
+
:step_id,
|
|
9
|
+
:branch_id
|
|
10
|
+
) do
|
|
11
|
+
def self.build(assembly_id:, assembly_kind:, participant:, step_id:, branch_id: nil) # :nodoc:
|
|
12
|
+
new(
|
|
13
|
+
assembly_id: assembly_id.to_s.dup.freeze,
|
|
14
|
+
assembly_kind: assembly_kind.to_sym,
|
|
15
|
+
participant: participant.to_s.dup.freeze,
|
|
16
|
+
step_id: step_id.to_s.dup.freeze,
|
|
17
|
+
branch_id: branch_id&.to_s&.dup&.freeze
|
|
18
|
+
).freeze
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Identifies one assembly step in the path to a streamed Agent invocation.
|
|
23
|
+
# Paths list outer steps before inner steps, so the final value identifies the
|
|
24
|
+
# participant that directly contains the Agent.
|
|
25
|
+
class AgentStreamStep < Data # :doc:
|
|
26
|
+
##
|
|
27
|
+
# :attr_reader: assembly_id
|
|
28
|
+
# Stable String identifier for the containing assembly.
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# :attr_reader: assembly_kind
|
|
32
|
+
# The +:workflow+, +:swarm+, +:graph+, or custom assembly kind.
|
|
33
|
+
|
|
34
|
+
##
|
|
35
|
+
# :attr_reader: participant
|
|
36
|
+
# String name used to route to this participant.
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# :attr_reader: step_id
|
|
40
|
+
# Unique String identifier for this assembly step.
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# :attr_reader: branch_id
|
|
44
|
+
# String branch identifier for a graph branch, or +nil+.
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
AgentStreamSource = Data.define( # :nodoc:
|
|
48
|
+
:agent_id,
|
|
49
|
+
:agent_path,
|
|
50
|
+
:operation_id,
|
|
51
|
+
:parent_operation_id,
|
|
52
|
+
:assembly_path
|
|
53
|
+
) do
|
|
54
|
+
def self.build(agent_id:, agent_path:, operation_id:, parent_operation_id:, assembly_path:) # :nodoc:
|
|
55
|
+
new(
|
|
56
|
+
agent_id: agent_id.to_s.dup.freeze,
|
|
57
|
+
agent_path: agent_path.to_s.dup.freeze,
|
|
58
|
+
operation_id: operation_id.to_s.dup.freeze,
|
|
59
|
+
parent_operation_id: parent_operation_id&.to_s&.dup&.freeze,
|
|
60
|
+
assembly_path: Array(assembly_path).dup.freeze
|
|
61
|
+
).freeze
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Describes which Agent produced an event during a Run.
|
|
66
|
+
#
|
|
67
|
+
# A +:agent_stream+ StreamEvent carries this value in +data[:source]+ and the
|
|
68
|
+
# Agent's event as a separate frozen StreamEvent in +data[:event]+.
|
|
69
|
+
# +assembly_path+ is empty for a top-level Agent and contains one
|
|
70
|
+
# AgentStreamStep for each enclosing composite assembly participant.
|
|
71
|
+
class AgentStreamSource < Data # :doc:
|
|
72
|
+
##
|
|
73
|
+
# :attr_reader: agent_id
|
|
74
|
+
# Stable String identifier declared by the Agent class.
|
|
75
|
+
|
|
76
|
+
##
|
|
77
|
+
# :attr_reader: agent_path
|
|
78
|
+
# Stable subagent path, beginning at <tt>/root</tt>.
|
|
79
|
+
|
|
80
|
+
##
|
|
81
|
+
# :attr_reader: operation_id
|
|
82
|
+
# Unique String identifier for this Agent invocation.
|
|
83
|
+
|
|
84
|
+
##
|
|
85
|
+
# :attr_reader: parent_operation_id
|
|
86
|
+
# Parent operation identifier, or +nil+ when the caller did not supply one.
|
|
87
|
+
|
|
88
|
+
##
|
|
89
|
+
# :attr_reader: assembly_path
|
|
90
|
+
# Frozen Array of AgentStreamStep values from the outermost assembly inward.
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
module AgentStreamSnapshot # :nodoc: all
|
|
94
|
+
module_function
|
|
95
|
+
|
|
96
|
+
def event(event)
|
|
97
|
+
data = copy(event.data)
|
|
98
|
+
freeze_value(StreamEvent.build(event.type, **data))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def message(message)
|
|
102
|
+
freeze_value(copy(message))
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def copy(value, copies = {})
|
|
106
|
+
return value if immutable_scalar?(value)
|
|
107
|
+
return copies.fetch(value.object_id) if copies.key?(value.object_id)
|
|
108
|
+
|
|
109
|
+
case value
|
|
110
|
+
when Message
|
|
111
|
+
copy_message(value, copies)
|
|
112
|
+
when DataMap
|
|
113
|
+
copy_data_map(value, copies)
|
|
114
|
+
when Hash
|
|
115
|
+
copy_hash(value, copies)
|
|
116
|
+
when Array
|
|
117
|
+
copy_array(value, copies)
|
|
118
|
+
when String
|
|
119
|
+
value.dup.freeze
|
|
120
|
+
when Data
|
|
121
|
+
copy_data(value, copies)
|
|
122
|
+
when Exception
|
|
123
|
+
copy_exception(value, copies)
|
|
124
|
+
else
|
|
125
|
+
copy_object(value, copies)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def copy_message(value, copies)
|
|
130
|
+
content = copy_array(value.content, copies)
|
|
131
|
+
metadata = copy_data_map(value.metadata, copies)
|
|
132
|
+
Message.new(role: value.role, content:, metadata:).freeze.tap do |snapshot|
|
|
133
|
+
copies[value.object_id] = snapshot
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def copy_data_map(value, copies)
|
|
138
|
+
snapshot = DataMap.new
|
|
139
|
+
copies[value.object_id] = snapshot
|
|
140
|
+
value.each do |key, child|
|
|
141
|
+
snapshot[key] = copy(child, copies)
|
|
142
|
+
end
|
|
143
|
+
snapshot.freeze
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def copy_hash(value, copies)
|
|
147
|
+
snapshot = {}
|
|
148
|
+
copies[value.object_id] = snapshot
|
|
149
|
+
value.each do |key, child|
|
|
150
|
+
snapshot[copy(key, copies)] = copy(child, copies)
|
|
151
|
+
end
|
|
152
|
+
snapshot.freeze
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def copy_array(value, copies)
|
|
156
|
+
snapshot = []
|
|
157
|
+
copies[value.object_id] = snapshot
|
|
158
|
+
value.each { |child| snapshot << copy(child, copies) }
|
|
159
|
+
snapshot.freeze
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def copy_data(value, copies)
|
|
163
|
+
attributes = value.members.to_h do |member|
|
|
164
|
+
[member, copy(value.public_send(member), copies)]
|
|
165
|
+
end
|
|
166
|
+
value.class.new(**attributes).freeze.tap do |snapshot|
|
|
167
|
+
copies[value.object_id] = snapshot
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def copy_object(value, copies)
|
|
172
|
+
return value if value.is_a?(Module)
|
|
173
|
+
|
|
174
|
+
snapshot = value.dup
|
|
175
|
+
copies[value.object_id] = snapshot
|
|
176
|
+
value.instance_variables.each do |name|
|
|
177
|
+
snapshot.instance_variable_set(name, copy(value.instance_variable_get(name), copies))
|
|
178
|
+
end
|
|
179
|
+
snapshot.freeze
|
|
180
|
+
rescue TypeError
|
|
181
|
+
value.frozen? ? value : value.to_s.dup.freeze
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def copy_exception(value, copies)
|
|
185
|
+
message = value.message.to_s.dup.freeze
|
|
186
|
+
snapshot = value.exception(message)
|
|
187
|
+
if snapshot.equal?(value)
|
|
188
|
+
snapshot = value.class.allocate
|
|
189
|
+
Exception.instance_method(:initialize).bind_call(snapshot, message)
|
|
190
|
+
end
|
|
191
|
+
copies[value.object_id] = snapshot
|
|
192
|
+
if value.cause
|
|
193
|
+
cause = copy(value.cause, copies)
|
|
194
|
+
begin
|
|
195
|
+
raise snapshot, cause:
|
|
196
|
+
rescue => raised
|
|
197
|
+
snapshot = raised
|
|
198
|
+
copies[value.object_id] = snapshot
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
value.instance_variables.each do |name|
|
|
202
|
+
snapshot.instance_variable_set(name, copy(value.instance_variable_get(name), copies))
|
|
203
|
+
end
|
|
204
|
+
backtrace = copy(value.backtrace, copies)
|
|
205
|
+
snapshot.set_backtrace(backtrace)
|
|
206
|
+
freeze_value(snapshot.backtrace) if snapshot.backtrace
|
|
207
|
+
snapshot.freeze
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def immutable_scalar?(value)
|
|
211
|
+
value.nil? || value == true || value == false || value.is_a?(Numeric) || value.is_a?(Symbol)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def freeze_value(value, seen = {})
|
|
215
|
+
return value if immutable_scalar?(value) || value.is_a?(Module)
|
|
216
|
+
return value if seen[value.object_id]
|
|
217
|
+
|
|
218
|
+
seen[value.object_id] = true
|
|
219
|
+
case value
|
|
220
|
+
when Message
|
|
221
|
+
freeze_value(value.content, seen)
|
|
222
|
+
freeze_value(value.metadata, seen)
|
|
223
|
+
when Hash
|
|
224
|
+
value.each do |key, child|
|
|
225
|
+
freeze_value(key, seen)
|
|
226
|
+
freeze_value(child, seen)
|
|
227
|
+
end
|
|
228
|
+
when Array
|
|
229
|
+
value.each { |child| freeze_value(child, seen) }
|
|
230
|
+
when Data
|
|
231
|
+
value.members.each { |member| freeze_value(value.public_send(member), seen) }
|
|
232
|
+
else
|
|
233
|
+
value.instance_variables.each do |name|
|
|
234
|
+
freeze_value(value.instance_variable_get(name), seen)
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
value.freeze
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
class AgentStreamSink # :nodoc: all
|
|
242
|
+
def initialize(destination:, run:, source:, input:)
|
|
243
|
+
@destination = destination
|
|
244
|
+
@run = run
|
|
245
|
+
@source = source
|
|
246
|
+
@input = input
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def <<(event)
|
|
250
|
+
data = {source: @source, event: AgentStreamSnapshot.event(event)}
|
|
251
|
+
data[:input] = AgentStreamSnapshot.message(normalized_input) if event.type == :invocation_start
|
|
252
|
+
@run.publish(:agent_stream, **data)
|
|
253
|
+
@destination << event
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
private
|
|
257
|
+
|
|
258
|
+
def normalized_input
|
|
259
|
+
@input.is_a?(Message) ? @input : Message.new(role: :user, content: @input)
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LittleGhost
|
|
4
|
+
# Represents a file, image, or document produced by a Tool or supplied to a
|
|
5
|
+
# Run. LittleGhost sends supported media to the model once and may store the
|
|
6
|
+
# same bytes for filesystem Tools. Inline artifacts contain their bytes.
|
|
7
|
+
# Deferred artifacts contain an application-defined reference that the block
|
|
8
|
+
# passed to Configuration#artifacts may use to load the bytes.
|
|
9
|
+
#
|
|
10
|
+
# image = LittleGhost::Artifact.new(
|
|
11
|
+
# data: File.binread("chart.png"),
|
|
12
|
+
# media_type: "image/png",
|
|
13
|
+
# name: "chart.png"
|
|
14
|
+
# )
|
|
15
|
+
#
|
|
16
|
+
# download = LittleGhost::Artifact.deferred(
|
|
17
|
+
# reference: {file_id: "file-481"},
|
|
18
|
+
# media_type: "application/pdf",
|
|
19
|
+
# name: "report.pdf"
|
|
20
|
+
# )
|
|
21
|
+
class Artifact
|
|
22
|
+
# Creates an inline artifact from binary +data+ and a MIME +media_type+.
|
|
23
|
+
def initialize(data:, media_type:, name: nil, metadata: {})
|
|
24
|
+
data = String(data).b
|
|
25
|
+
initialize_fields(
|
|
26
|
+
data: data.freeze,
|
|
27
|
+
reference: nil,
|
|
28
|
+
media_type:,
|
|
29
|
+
name:,
|
|
30
|
+
bytes: data.bytesize,
|
|
31
|
+
metadata:
|
|
32
|
+
)
|
|
33
|
+
rescue TypeError
|
|
34
|
+
raise ArgumentError, "artifact data must be a string"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Creates an artifact whose bytes may be loaded later by the block passed to
|
|
38
|
+
# Configuration#artifacts.
|
|
39
|
+
def self.deferred(reference:, media_type:, name: nil, metadata: {})
|
|
40
|
+
raise ArgumentError, "artifact reference is required" if reference.nil?
|
|
41
|
+
reference = immutable_reference(reference)
|
|
42
|
+
|
|
43
|
+
allocate.tap do |artifact|
|
|
44
|
+
artifact.__send__(
|
|
45
|
+
:initialize_fields,
|
|
46
|
+
data: nil,
|
|
47
|
+
reference:,
|
|
48
|
+
media_type:,
|
|
49
|
+
name:,
|
|
50
|
+
bytes: nil,
|
|
51
|
+
metadata:
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Binary content for an inline artifact, otherwise nil.
|
|
57
|
+
attr_reader :data
|
|
58
|
+
# Application-defined deferred reference, or generated Workspace reference
|
|
59
|
+
# after storage; otherwise nil.
|
|
60
|
+
attr_reader :reference
|
|
61
|
+
# Optional display filename.
|
|
62
|
+
attr_reader :name
|
|
63
|
+
# MIME media type used to present the artifact.
|
|
64
|
+
attr_reader :media_type
|
|
65
|
+
# Known byte count, otherwise nil for an unresolved artifact.
|
|
66
|
+
attr_reader :bytes
|
|
67
|
+
# Deeply frozen application metadata.
|
|
68
|
+
attr_reader :metadata
|
|
69
|
+
|
|
70
|
+
# Whether this artifact contains its bytes directly.
|
|
71
|
+
def inline? = !data.nil?
|
|
72
|
+
|
|
73
|
+
# Whether this artifact requires an application resolver.
|
|
74
|
+
def deferred? = !reference.nil? && bytes.nil?
|
|
75
|
+
|
|
76
|
+
# Compares all immutable artifact fields.
|
|
77
|
+
def ==(other)
|
|
78
|
+
other.instance_of?(self.class) &&
|
|
79
|
+
[data, reference, name, media_type, bytes, metadata] ==
|
|
80
|
+
[other.data, other.reference, other.name, other.media_type, other.bytes, other.metadata]
|
|
81
|
+
end
|
|
82
|
+
# Uses the same field comparison when an artifact is a Hash key.
|
|
83
|
+
alias_method :eql?, :==
|
|
84
|
+
|
|
85
|
+
# Computes a Hash key from all immutable artifact fields.
|
|
86
|
+
def hash = [self.class, data, reference, name, media_type, bytes, metadata].hash
|
|
87
|
+
|
|
88
|
+
# Avoids placing bytes, names, metadata, or deferred references in diagnostics.
|
|
89
|
+
def inspect
|
|
90
|
+
kind = if inline?
|
|
91
|
+
"inline"
|
|
92
|
+
elsif deferred?
|
|
93
|
+
"deferred"
|
|
94
|
+
else
|
|
95
|
+
"stored"
|
|
96
|
+
end
|
|
97
|
+
"#<#{self.class} kind=#{kind.inspect} media_type=#{media_type.inspect} bytes=#{bytes.inspect}>"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
class << self
|
|
101
|
+
def materialized(reference:, bytes:, media_type:, name: nil, metadata: {}) # :nodoc:
|
|
102
|
+
reference = String(reference)
|
|
103
|
+
bytes = Integer(bytes)
|
|
104
|
+
raise ArgumentError, "artifact reference is required" if reference.empty?
|
|
105
|
+
raise ArgumentError, "artifact bytes must not be negative" if bytes.negative?
|
|
106
|
+
|
|
107
|
+
allocate.tap do |artifact|
|
|
108
|
+
artifact.__send__(
|
|
109
|
+
:initialize_fields,
|
|
110
|
+
data: nil,
|
|
111
|
+
reference: reference.freeze,
|
|
112
|
+
media_type:,
|
|
113
|
+
name:,
|
|
114
|
+
bytes:,
|
|
115
|
+
metadata:
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
rescue TypeError
|
|
119
|
+
raise ArgumentError, "materialized artifact fields are invalid"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def immutable_reference(value)
|
|
125
|
+
case value
|
|
126
|
+
when String
|
|
127
|
+
raise ArgumentError, "artifact reference is required" if value.empty?
|
|
128
|
+
|
|
129
|
+
value.dup.freeze
|
|
130
|
+
when Hash
|
|
131
|
+
value.each_with_object({}) do |(key, child), copy|
|
|
132
|
+
copy[immutable_reference(key)] = immutable_reference(child)
|
|
133
|
+
end.freeze
|
|
134
|
+
when Array
|
|
135
|
+
value.map { |child| immutable_reference(child) }.freeze
|
|
136
|
+
else
|
|
137
|
+
unless value.frozen?
|
|
138
|
+
raise ArgumentError, "artifact reference must be immutable"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
value
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
def initialize_fields(data:, reference:, media_type:, name:, bytes:, metadata:)
|
|
149
|
+
media_type = String(media_type)
|
|
150
|
+
raise ArgumentError, "artifact media_type is required" if media_type.empty?
|
|
151
|
+
|
|
152
|
+
name = String(name) if name
|
|
153
|
+
@data = data
|
|
154
|
+
@reference = reference
|
|
155
|
+
@name = name&.freeze
|
|
156
|
+
@media_type = media_type.freeze
|
|
157
|
+
@bytes = bytes
|
|
158
|
+
@metadata = deep_freeze(DataMap.new(metadata))
|
|
159
|
+
freeze
|
|
160
|
+
rescue TypeError
|
|
161
|
+
raise ArgumentError, "artifact fields are invalid"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def deep_freeze(value)
|
|
165
|
+
pending = [value]
|
|
166
|
+
until pending.empty?
|
|
167
|
+
current = pending.pop
|
|
168
|
+
case current
|
|
169
|
+
when Hash
|
|
170
|
+
current.each do |key, child|
|
|
171
|
+
key.freeze
|
|
172
|
+
pending << child
|
|
173
|
+
end
|
|
174
|
+
when Array
|
|
175
|
+
current.each { |child| pending << child }
|
|
176
|
+
end
|
|
177
|
+
current.freeze
|
|
178
|
+
end
|
|
179
|
+
value
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|