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,253 @@
|
|
|
1
|
+
# Running in Production
|
|
2
|
+
|
|
3
|
+
The Agent or Assembly you ran in a script can move into a controller, job, CLI, or service without changing shape. A long-running application usually adds stable model names, shared services, conversation history, background execution, and observability.
|
|
4
|
+
|
|
5
|
+
## Select models by application role
|
|
6
|
+
|
|
7
|
+
A direct target keeps a small definition self-contained:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class CustomerSupportAgent < LittleGhost::Agent
|
|
11
|
+
model "openrouter:openai/gpt-5.6-luna"
|
|
12
|
+
end
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
As an application grows, a **model role** gives that choice a stable application name:
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
# config/initializers/little_ghost.rb
|
|
19
|
+
LittleGhost.configure do |config|
|
|
20
|
+
config.providers = {
|
|
21
|
+
openrouter: {
|
|
22
|
+
adapter: :openrouter,
|
|
23
|
+
api_key: ENV.fetch("OPENROUTER_API_KEY")
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
config.models = {
|
|
27
|
+
customer_support: {
|
|
28
|
+
target: "openrouter:openai/gpt-5.6-luna",
|
|
29
|
+
settings: {temperature: 0.2}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
config.default_model = :customer_support
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class CustomerSupportAgent < LittleGhost::Agent
|
|
36
|
+
model :customer_support
|
|
37
|
+
end
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Provider connections and model roles can also live in YAML files under `config/little_ghost`, or in files you select explicitly. Values set in Ruby take priority. An explicitly selected file comes next, followed by conventional files and environment defaults. See `LittleGhost::Configuration` when you need every supported source and override.
|
|
41
|
+
|
|
42
|
+
Prompts, caller input and history, Tool results, and attachments may go to the
|
|
43
|
+
selected provider. Choose configured providers that are appropriate for that
|
|
44
|
+
data and its retention or residency needs.
|
|
45
|
+
|
|
46
|
+
## Configure once, call from anywhere
|
|
47
|
+
|
|
48
|
+
The `LittleGhost.configure` block above is the entire initializer. Controllers and jobs can call your Agent and Assembly classes directly.
|
|
49
|
+
|
|
50
|
+
Then call the Agent directly from a controller or job:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
class SupportQuestionsController < ApplicationController
|
|
54
|
+
def create
|
|
55
|
+
run = CustomerSupportAgent.ask(
|
|
56
|
+
params.require(:question),
|
|
57
|
+
actor_id: current_user.id,
|
|
58
|
+
context: {account_id: current_user.account_id}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if run.completed?
|
|
62
|
+
render json: {answer: run.response}
|
|
63
|
+
else
|
|
64
|
+
render json: {error: "Support request failed"}, status: :bad_gateway
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
On the first class-level call, LittleGhost prepares model resolution, loading, prompt lookup, persistence, hooks, and factories. Later calls reuse those application services automatically.
|
|
71
|
+
|
|
72
|
+
Each `.ask` creates a fresh top-level Run with fresh bound participants and Tools. Reusing application services does not create conversation history. Pass a stable `session_id` only when a later request should continue an earlier conversation.
|
|
73
|
+
|
|
74
|
+
The controller supplies identity and account access from authenticated application state. The model cannot replace those values through its prompt or tool arguments. A background job uses the same direct calling style.
|
|
75
|
+
|
|
76
|
+
Configure LittleGhost before the first Agent or Assembly call. Once application services start successfully, the configuration is locked so every request sees one stable setup.
|
|
77
|
+
|
|
78
|
+
## Preserve conversation with Sessions
|
|
79
|
+
|
|
80
|
+
A **Session** lets one request continue an earlier conversation. Pass the same session ID and trusted actor ID with each related call:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
run = CustomerSupportAgent.ask(
|
|
84
|
+
"What did we decide about my refund?",
|
|
85
|
+
session_id: "conversation-42",
|
|
86
|
+
actor_id: authenticated_user.id
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Take `actor_id` from authenticated application state. A session ID alone does not prove who the caller is, and a nil actor does not separate tenants. Built-in persistence drops system messages, temporary messages, and private reasoning. If you customize persistence, decide what else is safe to store.
|
|
91
|
+
|
|
92
|
+
A session is checkpointed when its store write succeeds. The in-memory store lasts only as long as one process. Choose a durable `SessionStore` when conversations must survive a restart or continue on another process.
|
|
93
|
+
|
|
94
|
+
{LittleGhost::SessionStores::Filesystem}[rdoc-ref:LittleGhost::SessionStores::Filesystem] is a built-in durable choice for a trusted local or shared filesystem. Set its root to the application-managed directory that holds session data:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
LittleGhost.configure do |config|
|
|
98
|
+
config.session_store = {
|
|
99
|
+
provider: LittleGhost::SessionStores::Filesystem,
|
|
100
|
+
root: "/var/lib/customer_support/sessions"
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Every Run has a session ID so LittleGhost can checkpoint its progress. If you do not supply one, LittleGhost generates a new ID for that call. Because your application does not reuse that generated ID, it does not create conversation continuity. A persistent SessionStore may still save working state under it before the Run finishes, so keep request context safe to store or filter sensitive fields in your store.
|
|
106
|
+
|
|
107
|
+
## Stream or supervise long-running work
|
|
108
|
+
|
|
109
|
+
`.stream_ask` runs on the caller's thread and yields `StreamEvent` values as the answer arrives:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
stream = CustomerSupportAgent.stream_ask(question)
|
|
113
|
+
|
|
114
|
+
run = stream.each do |event|
|
|
115
|
+
publish(event) if event.type == :text_delta
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
record_outcome(run.outcome, error_type: run.error&.class&.name)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Composite assembly streams include intermediate and nested Agent work as `:agent_stream` events. This default also applies to the event consumer passed to `start_execution`. Pass `include_agent_events: false` when only the ordinary public stream is needed.
|
|
122
|
+
|
|
123
|
+
> **Safety note:** Contextual events can include inputs, reasoning, Tool
|
|
124
|
+
> arguments and results, errors, and output from every participant. Check that
|
|
125
|
+
> the destination may see the complete Run, or filter the events before sending
|
|
126
|
+
> or storing them. LittleGhost's AG-UI adapter ignores these events unless the
|
|
127
|
+
> application translates them explicitly.
|
|
128
|
+
|
|
129
|
+
Use `start_execution` when the caller must stay free for other work, or when you want to deliver an interjection to an active response:
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
execution = agent.start_execution(message: question) do |event|
|
|
133
|
+
event_buffer << event
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
execution.interject(message: "Include the latest ledger entry")
|
|
137
|
+
execution.wait(deadline: Time.now + 30)
|
|
138
|
+
execution.run.completed?
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The event block runs on the worker thread, so keep it quick. Cancellation, deadlines, and `close` ask the work to stop; they cannot forcibly end arbitrary provider or tool code. They also cannot undo actions that already happened.
|
|
142
|
+
|
|
143
|
+
## Keep Tool permission checks in application code
|
|
144
|
+
|
|
145
|
+
A Tool schema checks the shape of model-supplied input. Your application still
|
|
146
|
+
owns permission checks, safe retries, rate limits, and auditing. An ordinary
|
|
147
|
+
Tool runs in the Ruby process; a Sandbox contains only file or child-process
|
|
148
|
+
work that deliberately passes through it.
|
|
149
|
+
|
|
150
|
+
Use the Tool binding's `run` to read current, application-established values
|
|
151
|
+
from `run.invocation.context`. Don't rely on model arguments for identity or
|
|
152
|
+
account membership.
|
|
153
|
+
|
|
154
|
+
Treat `RunContext#state` as mutable working and Session state. Revalidate anything restored from an earlier request. Synchronize access when parallel Tools share mutable state, or mark every Tool that reads or changes it as `exclusive true`.
|
|
155
|
+
|
|
156
|
+
A `ToolError` message is visible to the model, so keep it safe to share. LittleGhost hides unexpected exception messages from model-facing results.
|
|
157
|
+
|
|
158
|
+
When a step retries, its tool calls may happen again too. Prefer read-only work, idempotency keys, or operations that are safe to repeat.
|
|
159
|
+
|
|
160
|
+
[Tools](tools.md) develops this pattern from the first application Tool through
|
|
161
|
+
bindings, concurrency, sandbox delegation, and code mode.
|
|
162
|
+
|
|
163
|
+
## Choose workspace and sandbox behavior explicitly
|
|
164
|
+
|
|
165
|
+
A **Workspace** names the host paths associated with a Run. A **Sandbox**
|
|
166
|
+
controls filesystem operations and child processes deliberately sent through
|
|
167
|
+
it. A custom Ruby Tool stays in the application process unless it delegates
|
|
168
|
+
work to the bound Sandbox.
|
|
169
|
+
|
|
170
|
+
The dependency-free default is an application-root Workspace with `LittleGhost::Sandboxes::Unrestricted`. It is not process or network isolation. Select an enforcing backend explicitly when a model can influence commands; LittleGhost raises when that backend is unavailable instead of silently falling back:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
require "fileutils"
|
|
174
|
+
require "tmpdir"
|
|
175
|
+
|
|
176
|
+
LittleGhost.configure do |config|
|
|
177
|
+
config.workspace = lambda do |**|
|
|
178
|
+
root = Dir.mktmpdir("little-ghost-support-")
|
|
179
|
+
LittleGhost::Workspace.new(
|
|
180
|
+
root:,
|
|
181
|
+
teardown: lambda do |workspace:, **|
|
|
182
|
+
FileUtils.remove_entry_secure(workspace.root) if File.exist?(workspace.root)
|
|
183
|
+
end
|
|
184
|
+
)
|
|
185
|
+
end
|
|
186
|
+
config.sandbox = {
|
|
187
|
+
provider: :native,
|
|
188
|
+
files: {root: :read_write},
|
|
189
|
+
root_filesystem: :isolated,
|
|
190
|
+
environment: {inherit: false, set: {"LANG" => "C.UTF-8"}},
|
|
191
|
+
network: :none
|
|
192
|
+
}
|
|
193
|
+
end
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
The temporary Workspace above is useful when files should live for one Run. Use application-managed storage, with tenant isolation and concurrency control, when files must persist or several Runs share a root.
|
|
197
|
+
|
|
198
|
+
The Run opens a Runtime-created Workspace before its Sandbox and closes them in reverse order after every outcome. Closing a Workspace invokes its configured teardown; it does not delete files by default. Cleanup failures raise because LittleGhost cannot confirm a clean shutdown of every owned resource. Existing instances passed by the application remain caller-owned.
|
|
199
|
+
|
|
200
|
+
[Workspaces and Sandboxes](sandboxing.md) explains backend selection, logical
|
|
201
|
+
Workspace paths, files, process-only runtime paths, Scopes, filtered
|
|
202
|
+
networking, and deployment validation in depth.
|
|
203
|
+
|
|
204
|
+
[Code Mode](code_mode.md) applies that setup to model-authored Ruby or
|
|
205
|
+
optional JavaScript that composes the Agent's existing Tools.
|
|
206
|
+
|
|
207
|
+
## Protect data in telemetry
|
|
208
|
+
|
|
209
|
+
LittleGhost emits events as a request starts, calls a model or tool, moves between assembly steps, retries, and finishes. Instrumentation subscribers and OpenTelemetry exporters can send those events to your monitoring system.
|
|
210
|
+
|
|
211
|
+
An external telemetry service may receive application identifiers and event
|
|
212
|
+
data. Redact sensitive values before exporting them. Avoid attributes with many
|
|
213
|
+
unique values, such as raw order or request IDs.
|
|
214
|
+
|
|
215
|
+
A composite `RunResult` includes short step summaries and trajectory queries.
|
|
216
|
+
Keep detailed provider errors and sensitive diagnostics in your monitoring
|
|
217
|
+
system, not in model or user responses.
|
|
218
|
+
|
|
219
|
+
## Know what the Run closes and raises
|
|
220
|
+
|
|
221
|
+
One top-level Run owns the workspace and sandbox that LittleGhost creates for it, plus application resources registered with `run.register`. It closes those resources after success, failure, a partial response, or cancellation. Existing workspace or sandbox instances passed by the application remain caller-owned.
|
|
222
|
+
|
|
223
|
+
Ordinary execution failures appear on the Run and its final event. Cleanup,
|
|
224
|
+
event delivery, or instrumentation can still raise an exception when
|
|
225
|
+
LittleGhost cannot promise a clean ending.
|
|
226
|
+
|
|
227
|
+
## Advanced: work with Runtime directly
|
|
228
|
+
|
|
229
|
+
A Runtime is the internal home for shared model resolution, loading, persistence, hooks, and resource factories. Most applications never need to handle it: `LittleGhost.configure` and class-level `.ask` are enough.
|
|
230
|
+
|
|
231
|
+
Use `LittleGhost.runtime` when an extension needs the shared object itself. Construct a separate Runtime only when one process deliberately hosts an isolated LittleGhost setup:
|
|
232
|
+
|
|
233
|
+
```ruby
|
|
234
|
+
configuration = LittleGhost::Configuration.new(root: isolated_root)
|
|
235
|
+
runtime = LittleGhost::Runtime.new(configuration: configuration)
|
|
236
|
+
agent = CustomerSupportAgent.new(runtime: runtime)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
An explicit Runtime has its own independent configuration. It does not replace
|
|
240
|
+
LittleGhost's shared default.
|
|
241
|
+
|
|
242
|
+
One Runtime can serve independent calls from several threads. Each call gets
|
|
243
|
+
its own Run, participants, Tools, and Runtime-created Workspace and Sandbox. An
|
|
244
|
+
Agent or Assembly already bound to an active Run must stay with that Run.
|
|
245
|
+
|
|
246
|
+
Within one SessionStore instance, LittleGhost serializes calls sharing a
|
|
247
|
+
Session. Multi-process deployments need coordination from their store. Custom
|
|
248
|
+
stores and other shared extension objects may receive concurrent calls and
|
|
249
|
+
must be thread-safe.
|
|
250
|
+
|
|
251
|
+
Runtime has no shutdown step. Shared services supplied by the application keep their own lifecycle. Shut those services down with the rest of your application. If you installed process-wide instrumentation subscribers, flush or shut down `LittleGhost::Instrumentation` during application shutdown.
|
|
252
|
+
|
|
253
|
+
For exact constructors, options, events, extension contracts, and error behavior, continue into the API reference for `LittleGhost::Configuration`, `LittleGhost::Runtime`, `LittleGhost::Run`, `LittleGhost::Execution`, `LittleGhost::Session`, `LittleGhost::Tool`, and `LittleGhost::StreamEvent`.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Prompts as Views
|
|
2
|
+
|
|
3
|
+
A short prompt fits nicely inside an Agent class. As the instructions grow, move them into a **prompt view**: an ERB file that LittleGhost finds and renders for the Agent.
|
|
4
|
+
|
|
5
|
+
This keeps the Agent definition focused. It also gives shared instructions and application values a natural home.
|
|
6
|
+
|
|
7
|
+
## Start with the inline prompt
|
|
8
|
+
|
|
9
|
+
The Agent from Getting Started keeps its first instruction close to the model:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
class CustomerSupportAgent < LittleGhost::Agent
|
|
13
|
+
model "openrouter:openai/gpt-5.6-luna"
|
|
14
|
+
system_prompt "Answer customer questions clearly and concisely."
|
|
15
|
+
end
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Inline prompts are a good fit while the whole instruction is one thought.
|
|
19
|
+
|
|
20
|
+
## Move a growing prompt into a view
|
|
21
|
+
|
|
22
|
+
Remove `system_prompt` from the class:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
class CustomerSupportAgent < LittleGhost::Agent
|
|
26
|
+
model "openrouter:openai/gpt-5.6-luna"
|
|
27
|
+
tools HelpCenterLookupTool, OrderStatusTool
|
|
28
|
+
end
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then create `app/prompts/customer_support/system.erb`:
|
|
32
|
+
|
|
33
|
+
```erb
|
|
34
|
+
You help customers understand their orders and account.
|
|
35
|
+
|
|
36
|
+
Answer clearly and concisely.
|
|
37
|
+
Never invent company guidance. Check the help center when policy matters.
|
|
38
|
+
Use the order status tool before making a claim about a private order.
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That is enough. `CustomerSupportAgent` becomes `customer_support`, so LittleGhost looks for `customer_support/system.erb` under `app/prompts`.
|
|
42
|
+
|
|
43
|
+
The prompt is still a system instruction sent to the selected model provider. Keeping it in a view improves organization; it does not keep the content inside your process.
|
|
44
|
+
|
|
45
|
+
## Give the view application values
|
|
46
|
+
|
|
47
|
+
Use `prompt_local` for a value the application owns:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
class CustomerSupportAgent < LittleGhost::Agent
|
|
51
|
+
prompt_local :company_name, "Northstar"
|
|
52
|
+
end
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The local is available by name in the view:
|
|
56
|
+
|
|
57
|
+
```erb
|
|
58
|
+
You are a customer support agent for <%= company_name %>.
|
|
59
|
+
Answer clearly and concisely.
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
A block can resolve a trusted value for each Agent instance. Add it to the Agent class too:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
class CustomerSupportAgent < LittleGhost::Agent
|
|
66
|
+
prompt_local(:policy_version) { SupportPolicy.current_version }
|
|
67
|
+
end
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Prompt views also receive `invocation`, `run`, and `agent`. Reach for those when the instruction truly depends on the current request. Keep user wording in the caller message unless you deliberately want it inside the system instruction.
|
|
71
|
+
|
|
72
|
+
Every rendered value may be sent to the model provider. Pass only data that belongs in the prompt.
|
|
73
|
+
|
|
74
|
+
## Share a small partial
|
|
75
|
+
|
|
76
|
+
Partials keep repeated instructions in one place. Create `app/prompts/shared/_voice.erb`:
|
|
77
|
+
|
|
78
|
+
```erb
|
|
79
|
+
Use a warm, direct voice for <%= company_name %>.
|
|
80
|
+
Prefer one clear next step over a long list of possibilities.
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Render it from the Agent's system view:
|
|
84
|
+
|
|
85
|
+
```erb
|
|
86
|
+
You are a customer support agent for <%= company_name %>.
|
|
87
|
+
|
|
88
|
+
<%= partial "shared/voice", locals: {company_name: company_name} %>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The underscore marks a partial. Its locals are explicit, so it does not quietly inherit everything available to the parent view.
|
|
92
|
+
|
|
93
|
+
## Choose a different template path
|
|
94
|
+
|
|
95
|
+
Most named Agents can rely on their conventional path. Use `system_template` when a class should read a differently named view:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
class BillingSupportAgent < LittleGhost::Agent
|
|
99
|
+
system_template "customer_support/billing"
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
LittleGhost chooses one prompt source in this order:
|
|
104
|
+
|
|
105
|
+
1. An inline `system_prompt`
|
|
106
|
+
2. An explicit `system_template`
|
|
107
|
+
3. The Agent's conventional `system.erb` view
|
|
108
|
+
|
|
109
|
+
Applications can add prompt lookup roots through `Configuration#prompt_paths`.
|
|
110
|
+
Earlier roots win, which lets application code override a shared prompt package.
|
|
111
|
+
|
|
112
|
+
## Treat views as application code
|
|
113
|
+
|
|
114
|
+
Prompt views run as ERB inside the Ruby process and can call Ruby. Keep prompt
|
|
115
|
+
directories with the rest of your application code rather than letting a
|
|
116
|
+
request choose one.
|
|
117
|
+
|
|
118
|
+
`TrustedPath` is available for the uncommon case where application code selects
|
|
119
|
+
a request-specific root. It marks that choice explicitly; it does not inspect
|
|
120
|
+
or restrict the directory.
|
|
121
|
+
|
|
122
|
+
## Build request-specific input in a Workflow
|
|
123
|
+
|
|
124
|
+
A prompt view defines reusable instructions for one Agent. A Workflow may still build request-specific input for that Agent:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
invoke CustomerSupportAgent, input: <<~MESSAGE
|
|
128
|
+
#{input.text}
|
|
129
|
+
|
|
130
|
+
Verified research:
|
|
131
|
+
#{research}
|
|
132
|
+
MESSAGE
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The Workflow is composing this request. `CustomerSupportAgent` still receives its own system prompt view when it runs.
|
|
136
|
+
|
|
137
|
+
Continue with [Tools](tools.md) to give those Agents application capabilities
|
|
138
|
+
while keeping model input, trusted context, and delegated sandbox operations
|
|
139
|
+
distinct.
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Workspaces and Sandboxes
|
|
2
|
+
|
|
3
|
+
A Workspace gives one Run—one top-level Agent or Assembly execution—a stable
|
|
4
|
+
set of host paths. A Sandbox decides which operations may reach those paths and
|
|
5
|
+
how child processes are contained.
|
|
6
|
+
|
|
7
|
+
Use them together when an Agent can read files, change files, or run programs:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require "fileutils"
|
|
11
|
+
require "tmpdir"
|
|
12
|
+
|
|
13
|
+
LittleGhost.configure do |config|
|
|
14
|
+
config.workspace = lambda do |**|
|
|
15
|
+
root = Dir.mktmpdir("little-ghost-support-")
|
|
16
|
+
LittleGhost::Workspace.new(
|
|
17
|
+
root:,
|
|
18
|
+
teardown: lambda do |workspace:, **|
|
|
19
|
+
FileUtils.remove_entry_secure(workspace.root) if File.exist?(workspace.root)
|
|
20
|
+
end
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
config.sandbox = {
|
|
25
|
+
provider: :native,
|
|
26
|
+
files: {root: :read_write},
|
|
27
|
+
root_filesystem: :isolated,
|
|
28
|
+
environment: {inherit: false, set: {"LANG" => "C.UTF-8"}},
|
|
29
|
+
network: :none
|
|
30
|
+
}
|
|
31
|
+
end
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
This example gives each Run a temporary writable root and removes it during
|
|
35
|
+
teardown. The `:native` Sandbox backend selects Seatbelt on macOS or Bubblewrap on
|
|
36
|
+
Linux. It raises instead of running without isolation when the native backend
|
|
37
|
+
is unavailable.
|
|
38
|
+
|
|
39
|
+
`LittleGhost::Tools::Filesystem` and `LittleGhost::Tools::Shell` use the Sandbox
|
|
40
|
+
assigned to the current Run. Code-mode interpreters also run inside their own
|
|
41
|
+
Sandbox. Ordinary application Tools, callbacks, and provider requests stay in
|
|
42
|
+
the Ruby process unless they deliberately delegate an operation.
|
|
43
|
+
|
|
44
|
+
The split looks like this:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
Ruby application process
|
|
48
|
+
├── provider requests
|
|
49
|
+
├── application Tool#call
|
|
50
|
+
└── Sandbox
|
|
51
|
+
├── bounded filesystem operations
|
|
52
|
+
├── Shell child processes
|
|
53
|
+
└── code-mode interpreter processes
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Give files a stable home
|
|
57
|
+
|
|
58
|
+
A Workspace names the files available during a Run and controls their setup and
|
|
59
|
+
cleanup. Its `root` is the default working directory. Named paths give important
|
|
60
|
+
directories logical identities:
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
workspace = LittleGhost::Workspace.new(
|
|
64
|
+
root: "/var/lib/support/run-481",
|
|
65
|
+
paths: {
|
|
66
|
+
source: "/srv/support/source",
|
|
67
|
+
skills: "skills",
|
|
68
|
+
home: "runtime-home"
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Relative named paths live beneath the root and are created when the Workspace
|
|
74
|
+
opens. Absolute named paths are references deliberately supplied by the
|
|
75
|
+
application and must already exist. A setup callback can provision them when
|
|
76
|
+
needed.
|
|
77
|
+
|
|
78
|
+
Opening resolves real paths and records which physical directories they refer
|
|
79
|
+
to. LittleGhost rejects two names that point to the same directory and later
|
|
80
|
+
fails closed if a configured directory is replaced. Nested paths are allowed,
|
|
81
|
+
and the most restrictive matching access wins.
|
|
82
|
+
|
|
83
|
+
Workspace object lifetime and file lifetime are separate. Closing invokes the
|
|
84
|
+
configured teardown callback, but does not delete files by default. Use a
|
|
85
|
+
run-scoped temporary root for disposable work. Use application-managed storage,
|
|
86
|
+
tenant isolation, and concurrency control when several Runs share files.
|
|
87
|
+
|
|
88
|
+
## Give artifacts logical references
|
|
89
|
+
|
|
90
|
+
Images and documents normally remain provider content. When filesystem Tools
|
|
91
|
+
or code mode should read the same bytes, configure the conventional artifact
|
|
92
|
+
path and enable artifact handling:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
LittleGhost.configure do |config|
|
|
96
|
+
config.workspace = {
|
|
97
|
+
provider: :directory,
|
|
98
|
+
root: "tmp/agent-runs",
|
|
99
|
+
paths: {artifacts: "artifacts"}
|
|
100
|
+
}
|
|
101
|
+
config.artifacts
|
|
102
|
+
end
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
LittleGhost stores input images and documents after the Workspace and Sandbox
|
|
106
|
+
open. The model receives the image or document in its normal input without a
|
|
107
|
+
second text reference. Filesystem Tools can list `workspace://artifacts` when a
|
|
108
|
+
later operation needs the stored copy. Messages added while the Run is active
|
|
109
|
+
receive the same treatment.
|
|
110
|
+
|
|
111
|
+
LittleGhost limits the number and size of files stored from each message and
|
|
112
|
+
across the complete Run. Those limits also include Tool artifacts and oversized
|
|
113
|
+
Tool results. A successful operation stores all of its files. If storage fails
|
|
114
|
+
partway through, LittleGhost attempts to remove that operation's files and
|
|
115
|
+
reports a cleanup error when it cannot. Stored files use private permissions,
|
|
116
|
+
and the Workspace provider still owns final cleanup.
|
|
117
|
+
|
|
118
|
+
Declaring the path does not grant model access. When the Agent should read it,
|
|
119
|
+
grant the Sandbox access to `:artifacts` and include a filesystem Tool.
|
|
120
|
+
|
|
121
|
+
## Pass logical paths, not host layout
|
|
122
|
+
|
|
123
|
+
The Filesystem Tool accepts root-relative paths such as `notes/today.md`.
|
|
124
|
+
Named paths use references such as
|
|
125
|
+
`workspace://skills/refunds/SKILL.md`.
|
|
126
|
+
|
|
127
|
+
Those references remain meaningful without exposing or translating the host
|
|
128
|
+
layout. The Filesystem Tool rejects physical absolute paths. Child processes
|
|
129
|
+
use the Workspace's real paths directly, start in its root, and receive
|
|
130
|
+
`LITTLE_GHOST_WORKSPACE_ROOT` plus one
|
|
131
|
+
`LITTLE_GHOST_WORKSPACE_<NAME>` variable for each named path.
|
|
132
|
+
|
|
133
|
+
Workspace references give application code one stable path format. Each
|
|
134
|
+
Sandbox backend applies the configured access using the filesystem controls
|
|
135
|
+
available on its host.
|
|
136
|
+
|
|
137
|
+
## Separate Tool-visible files from process support
|
|
138
|
+
|
|
139
|
+
Sandbox configuration divides named paths into two groups:
|
|
140
|
+
|
|
141
|
+
```ruby
|
|
142
|
+
config.sandbox = {
|
|
143
|
+
provider: :native,
|
|
144
|
+
files: {
|
|
145
|
+
root: :read_write,
|
|
146
|
+
source: :read_only,
|
|
147
|
+
skills: :read_only
|
|
148
|
+
},
|
|
149
|
+
runtime_paths: {
|
|
150
|
+
home: :read_write
|
|
151
|
+
},
|
|
152
|
+
network: :none
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`files` are visible to both the Filesystem Tool and sandboxed processes.
|
|
157
|
+
`runtime_paths` are process-only. Use runtime paths for interpreter libraries,
|
|
158
|
+
homes, sockets, and service state that a model should not browse through a
|
|
159
|
+
filesystem Tool.
|
|
160
|
+
|
|
161
|
+
This is visibility, not secrecy from the process. A child with a runtime-path
|
|
162
|
+
grant can use that path according to its access mode. The distinction prevents
|
|
163
|
+
the Filesystem Tool from offering it as a model-visible file
|
|
164
|
+
tree.
|
|
165
|
+
|
|
166
|
+
## Narrow access with a Scope
|
|
167
|
+
|
|
168
|
+
A `Sandbox::Scope` is a non-owning, reduced view of a Sandbox. It can remove
|
|
169
|
+
paths, change writable access to read-only, remove capabilities (categories of
|
|
170
|
+
allowed operations), or narrow networking. It cannot widen its parent:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
reviewer = run.sandbox.scope(
|
|
174
|
+
files: {source: :read_only},
|
|
175
|
+
runtime_paths: [],
|
|
176
|
+
capabilities: %i[filesystem_read filesystem_list],
|
|
177
|
+
network: false
|
|
178
|
+
)
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Scopes constrain only code that receives and uses the Scope. Code that keeps a
|
|
182
|
+
reference to the parent Sandbox keeps the parent's authority. A Scope does not
|
|
183
|
+
open or close its parent and owns no resources.
|
|
184
|
+
|
|
185
|
+
## Choose how much of the host exists
|
|
186
|
+
|
|
187
|
+
`root_filesystem` controls what a sandboxed process can see beyond declared
|
|
188
|
+
Workspace paths:
|
|
189
|
+
|
|
190
|
+
- `:isolated` exposes only declared paths and required runtime support. It is
|
|
191
|
+
the default and the right starting point for generated interpreters.
|
|
192
|
+
- `:read_only` exposes the host filesystem for development commands while
|
|
193
|
+
confining writes to declared writable paths. It makes installed compilers,
|
|
194
|
+
package managers, profiles, and toolchains available, but the child can also
|
|
195
|
+
read host files unless the Ruby process already runs inside a container or VM
|
|
196
|
+
that prevents those reads.
|
|
197
|
+
- `:read_write` grants the host filesystem directly. Treat it as unrestricted
|
|
198
|
+
host authority.
|
|
199
|
+
|
|
200
|
+
On Seatbelt, host-visible modes permit subprocesses because development tools
|
|
201
|
+
often need them. A Scope can remove `process_spawn` for a command that does not.
|
|
202
|
+
|
|
203
|
+
## Choose an enforcement backend
|
|
204
|
+
|
|
205
|
+
| Sandbox backend | Host | What it enforces |
|
|
206
|
+
| --- | --- | --- |
|
|
207
|
+
| `:native` | macOS or Linux | Selects Seatbelt on macOS and Bubblewrap on Linux; fails closed elsewhere |
|
|
208
|
+
| `:seatbelt` | macOS | Deny-default Seatbelt profile over the configured physical paths |
|
|
209
|
+
| `:bubblewrap` | Linux | Fresh user, PID, mount, IPC, UTS, and optional network namespaces |
|
|
210
|
+
| `:unrestricted` | Ruby platforms | No containment; commands have the application process's host authority |
|
|
211
|
+
|
|
212
|
+
`LittleGhost::Sandbox.probe(:native)` reports whether the platform backend is
|
|
213
|
+
available. Selecting an enforcing backend never falls back to unrestricted
|
|
214
|
+
execution.
|
|
215
|
+
|
|
216
|
+
Bubblewrap owns descendants with a PID namespace and ends them when the
|
|
217
|
+
supervising process dies. It cannot selectively deny fork inside that
|
|
218
|
+
namespace, so a request for
|
|
219
|
+
`allow_subprocesses: false` fails closed rather than claiming an unenforced
|
|
220
|
+
restriction. Bubblewrap does not impose a task-count limit by itself. Use an
|
|
221
|
+
outer cgroup or container supervisor when generated code needs a hard cap on
|
|
222
|
+
the processes and threads it can create.
|
|
223
|
+
|
|
224
|
+
Seatbelt can constrain spawned children, but macOS has no PID namespace. It
|
|
225
|
+
terminates the command process group during cleanup; a child that deliberately
|
|
226
|
+
detaches from that group may survive. Use an outer process or container
|
|
227
|
+
supervisor when complete descendant ownership is required on macOS.
|
|
228
|
+
|
|
229
|
+
> **Safety note:** `LittleGhost::Sandboxes::Unrestricted` is suitable for
|
|
230
|
+
> application commands and tests that you would already run directly. It
|
|
231
|
+
> validates paths and bounds output, but it does not isolate the command from
|
|
232
|
+
> the host. Use `:native` for generated commands in production.
|
|
233
|
+
|
|
234
|
+
## Keep process ownership explicit
|
|
235
|
+
|
|
236
|
+
`Sandbox#start_program` returns a `Sandbox::ProcessSession` with bounded input
|
|
237
|
+
and output, `alive?`, bounded `wait(timeout:)`, `terminate`, and `close`. The
|
|
238
|
+
session starts the command in a process group so cleanup can stop it and its
|
|
239
|
+
ordinary descendants together. It applies available CPU, memory, file, and
|
|
240
|
+
output limits, requests termination, and then forces termination when needed.
|
|
241
|
+
|
|
242
|
+
Callers that open a ProcessSession own it and must close it. LittleGhost fails
|
|
243
|
+
closed when it cannot supervise a configured memory limit. The parent samples
|
|
244
|
+
the visible process tree every 100 milliseconds, so it reacts only to memory
|
|
245
|
+
present at a sample and may miss peaks between samples. On Linux, three
|
|
246
|
+
consecutive failures to read the root process or the `/proc` snapshot end the
|
|
247
|
+
process. Use an outer cgroup or container when memory must be enforced as a hard
|
|
248
|
+
limit by the operating system.
|
|
249
|
+
|
|
250
|
+
A Run closes the Workspace and Sandbox it creates after success, failure, a
|
|
251
|
+
partial response, or cancellation. Application-supplied instances remain
|
|
252
|
+
caller-owned.
|
|
253
|
+
|
|
254
|
+
## Configure child-process networking separately
|
|
255
|
+
|
|
256
|
+
Sandbox networking has three modes:
|
|
257
|
+
|
|
258
|
+
- `:none` removes child networking.
|
|
259
|
+
- `:inherit` permits the selected Sandbox backend's ordinary network access.
|
|
260
|
+
- `:allowlist` requires an enforcing gateway: a supervised proxy that permits
|
|
261
|
+
only configured destinations.
|
|
262
|
+
|
|
263
|
+
Proxy environment variables alone are not an allowlist. An external gateway
|
|
264
|
+
uses named, process-only Workspace paths and verifies that they still point to
|
|
265
|
+
the configured directories; it does not create a virtual path mapping.
|
|
266
|
+
LittleGhost does not attest that an external proxy is ready or enforcing the
|
|
267
|
+
declared destinations. The application must supervise and verify that
|
|
268
|
+
gateway before giving a child network access.
|
|
269
|
+
|
|
270
|
+
These settings reach only processes launched through the Sandbox. Providers,
|
|
271
|
+
callbacks, and application Tool code still use the Ruby process's network
|
|
272
|
+
access.
|
|
273
|
+
|
|
274
|
+
Test the deployed kernel and filesystem, not only the configuration object.
|
|
275
|
+
Exercise denied reads and writes, runtime paths, child creation, direct sockets,
|
|
276
|
+
cancellation, limits, and cleanup before depending on the Sandbox to isolate
|
|
277
|
+
generated code.
|
|
278
|
+
|
|
279
|
+
Continue with [Code Mode](code_mode.md) to see how a model-authored interpreter
|
|
280
|
+
uses this setup while every Tool call stays in the parent Ruby process. For
|
|
281
|
+
exact setup, access, and cleanup behavior, see `LittleGhost::Workspace`,
|
|
282
|
+
`LittleGhost::Sandbox`, and `LittleGhost::Sandbox::Scope`.
|