little_ghost 0.3.0 → 0.4.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 +68 -84
- data/docs/guides/assemblies.md +286 -0
- data/docs/guides/core_concepts.md +126 -231
- data/docs/guides/getting_started.md +114 -87
- data/docs/guides/production.md +187 -0
- data/docs/guides/prompt_views.md +132 -0
- data/lib/little_ghost/ag_ui/adapter.rb +3 -3
- data/lib/little_ghost/agent/delegation.rb +1 -1
- data/lib/little_ghost/agent.rb +167 -172
- data/lib/little_ghost/{agent_interruptions.rb → agent_interjections.rb} +12 -12
- data/lib/little_ghost/assembly.rb +55 -21
- data/lib/little_ghost/assembly_builder.rb +40 -2
- data/lib/little_ghost/assembly_execution.rb +87 -4
- data/lib/little_ghost/configuration.rb +263 -39
- data/lib/little_ghost/content.rb +5 -5
- data/lib/little_ghost/data_map.rb +209 -0
- data/lib/little_ghost/errors.rb +2 -2
- data/lib/little_ghost/execution.rb +32 -32
- data/lib/little_ghost/graph.rb +22 -3
- data/lib/little_ghost/message.rb +4 -4
- data/lib/little_ghost/model_resolver.rb +2 -2
- data/lib/little_ghost/prompt_resolver.rb +2 -0
- data/lib/little_ghost/run.rb +87 -49
- data/lib/little_ghost/run_context.rb +33 -20
- data/lib/little_ghost/runtime/hook.rb +3 -3
- data/lib/little_ghost/runtime.rb +71 -31
- data/lib/little_ghost/session.rb +12 -23
- 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/subagents/manager.rb +42 -42
- data/lib/little_ghost/swarm.rb +13 -5
- data/lib/little_ghost/tool.rb +56 -14
- 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.rb +29 -25
- metadata +7 -2
|
@@ -1,33 +1,62 @@
|
|
|
1
|
-
# Getting Started
|
|
1
|
+
# Getting Started
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
In this guide, you'll run an agent, connect it to a small help center, and stream its answer. The whole feature stays in ordinary Ruby.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Install the gem
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
LittleGhost requires Ruby 3.3 or newer. Add the gem to your `Gemfile`:
|
|
7
|
+
LittleGhost requires Ruby 3.3 or newer. Add the gem to your `Gemfile`, install it, and set a provider credential:
|
|
10
8
|
|
|
11
9
|
```ruby
|
|
12
10
|
gem "little_ghost"
|
|
13
11
|
```
|
|
14
12
|
|
|
15
|
-
Install the bundle and set a provider credential:
|
|
16
|
-
|
|
17
13
|
```sh
|
|
18
14
|
$ bundle install
|
|
19
|
-
$ export
|
|
15
|
+
$ export OPENROUTER_API_KEY="..."
|
|
20
16
|
```
|
|
21
17
|
|
|
22
|
-
|
|
18
|
+
Use your application's secret manager outside a local shell, and never commit provider credentials.
|
|
19
|
+
|
|
20
|
+
This guide uses OpenRouter because one credential is enough to begin. LittleGhost can use other provider connections too; you will configure those in [Running in Production](production.md).
|
|
23
21
|
|
|
24
|
-
##
|
|
22
|
+
## See your first answer
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
Create `customer_support_agent.rb`:
|
|
27
25
|
|
|
28
26
|
```ruby
|
|
29
27
|
require "little_ghost"
|
|
30
28
|
|
|
29
|
+
class CustomerSupportAgent < LittleGhost::Agent
|
|
30
|
+
model "openrouter:openai/gpt-5.6-luna"
|
|
31
|
+
system_prompt "Answer customer questions clearly and concisely."
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
run = CustomerSupportAgent.ask("Can I change the address on my order?")
|
|
35
|
+
|
|
36
|
+
if run.completed?
|
|
37
|
+
puts run.response
|
|
38
|
+
else
|
|
39
|
+
warn "Support request ended as #{run.outcome}: #{run.error&.class}"
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Run the file and you have a working AI feature:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
$ ruby customer_support_agent.rb
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`CustomerSupportAgent.ask` creates a `LittleGhost::Run` for this request. When the work finishes, the Run holds the outcome and response.
|
|
50
|
+
|
|
51
|
+
The inline prompt keeps this first example easy to see in one place. When the instructions grow, [Prompts as Views](prompt_views.md) moves them into a conventional ERB file without adding setup to the Agent.
|
|
52
|
+
|
|
53
|
+
The selected external provider may receive system instructions, caller input, conversation history, tool results, and attachments. Model wording can vary, so use application code—not a prompt—when a rule must always hold.
|
|
54
|
+
|
|
55
|
+
## Connect the agent to your application
|
|
56
|
+
|
|
57
|
+
The first agent can answer general questions. A **tool** gives it a focused operation backed by your Ruby code:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
31
60
|
class HelpCenterLookupTool < LittleGhost::Tool
|
|
32
61
|
HELP_CENTER_ENTRIES = {
|
|
33
62
|
"refunds" => "Refunds are available within 30 days of purchase.",
|
|
@@ -50,119 +79,117 @@ class HelpCenterLookupTool < LittleGhost::Tool
|
|
|
50
79
|
end
|
|
51
80
|
```
|
|
52
81
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
The schema checks shape, not authorization. A tool that reads customer data or performs an action must enforce the application's trust rules inside its implementation.
|
|
56
|
-
|
|
57
|
-
## Define the agent
|
|
58
|
-
|
|
59
|
-
Now describe the agent's behavior and make the lookup available to it:
|
|
82
|
+
Make the tool available to the agent and tell the model when to use it:
|
|
60
83
|
|
|
61
84
|
```ruby
|
|
62
85
|
class CustomerSupportAgent < LittleGhost::Agent
|
|
63
86
|
description "Answers customer support questions."
|
|
64
|
-
model "openai
|
|
87
|
+
model "openrouter:openai/gpt-5.6-luna"
|
|
65
88
|
system_prompt <<~PROMPT
|
|
66
89
|
Answer clearly and do not invent company guidance.
|
|
67
|
-
|
|
90
|
+
Check the help center before stating company guidance.
|
|
68
91
|
PROMPT
|
|
69
|
-
|
|
70
92
|
tools HelpCenterLookupTool
|
|
71
93
|
end
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
An agent class is a reusable behavior definition. It owns its prompt, tools, model selection, limits, and other capabilities. This agent names an OpenAI connection and model directly, so the first example does not need a separate model profile.
|
|
75
94
|
|
|
76
|
-
## Ask a question
|
|
77
|
-
|
|
78
|
-
Call the agent class to run one request to completion:
|
|
79
|
-
|
|
80
|
-
```ruby
|
|
81
95
|
run = CustomerSupportAgent.ask(
|
|
82
96
|
"I bought an item two weeks ago. Can I get a refund?"
|
|
83
97
|
)
|
|
84
98
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
warn "Support request ended as #{run.outcome}: #{run.error&.class}"
|
|
89
|
-
end
|
|
99
|
+
run.response
|
|
100
|
+
# One possible response:
|
|
101
|
+
# Refunds are available within 30 days, so your purchase is eligible.
|
|
90
102
|
```
|
|
91
103
|
|
|
92
|
-
|
|
104
|
+
LittleGhost checks the model's arguments before it calls `HelpCenterLookupTool#call`. The schema checks shape, not permission. If a tool reads customer data or changes something, authorize that work from trusted application context. The tool's result then becomes context for the model.
|
|
93
105
|
|
|
94
|
-
|
|
106
|
+
### Use trusted context for private data
|
|
95
107
|
|
|
96
|
-
|
|
97
|
-
Refunds are available within 30 days, so a purchase from two weeks ago is eligible.
|
|
98
|
-
```
|
|
108
|
+
Model tool arguments are untrusted, even after their shape has been checked. Pass identity and permissions from your application's authentication boundary instead.
|
|
99
109
|
|
|
100
|
-
|
|
110
|
+
While an Agent is working, LittleGhost binds each Tool instance to the current Run. The Tool can read trusted request values through its `run` accessor:
|
|
101
111
|
|
|
102
|
-
|
|
112
|
+
```ruby
|
|
113
|
+
class OrderStatusTool < LittleGhost::Tool
|
|
114
|
+
ORDER_STATUSES = {
|
|
115
|
+
["user-7", "account-2", "481"] => "out for delivery"
|
|
116
|
+
}.freeze
|
|
103
117
|
|
|
104
|
-
|
|
118
|
+
description "Look up an order that belongs to the current customer."
|
|
119
|
+
input_schema(
|
|
120
|
+
type: "object",
|
|
121
|
+
properties: {order_number: {type: "string"}},
|
|
122
|
+
required: ["order_number"],
|
|
123
|
+
additionalProperties: false
|
|
124
|
+
)
|
|
105
125
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
126
|
+
def call(input)
|
|
127
|
+
lookup = [
|
|
128
|
+
run.invocation.actor_id,
|
|
129
|
+
run.invocation.context.fetch("account_id"),
|
|
130
|
+
input.fetch("order_number")
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
ORDER_STATUSES.fetch(lookup) do
|
|
134
|
+
raise LittleGhost::ToolError, "Order not found"
|
|
135
|
+
end
|
|
113
136
|
end
|
|
114
137
|
end
|
|
138
|
+
|
|
139
|
+
class CustomerSupportAgent < LittleGhost::Agent
|
|
140
|
+
tools HelpCenterLookupTool, OrderStatusTool
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
run = CustomerSupportAgent.ask(
|
|
144
|
+
"Where is order 481?",
|
|
145
|
+
actor_id: "user-7",
|
|
146
|
+
context: {account_id: "account-2"}
|
|
147
|
+
)
|
|
115
148
|
```
|
|
116
149
|
|
|
117
|
-
The
|
|
150
|
+
Here, `order_number` came from the model. The application supplied `actor_id` and `account_id` after authenticating the caller. LittleGhost places those request values on `run.invocation`; context keys become strings. The model cannot replace them through its tool arguments.
|
|
118
151
|
|
|
119
|
-
|
|
152
|
+
That is enough to authorize the first Tool safely. [Core Concepts](core_concepts.md) names the request and working-state objects behind `run`, and [Running in Production](production.md) explains what changes when you add saved conversations.
|
|
120
153
|
|
|
121
|
-
##
|
|
154
|
+
## Stream the same agent
|
|
122
155
|
|
|
123
|
-
|
|
156
|
+
Use `.stream_ask` when a console, HTTP response, or user interface should receive progress as it happens:
|
|
124
157
|
|
|
125
158
|
```ruby
|
|
126
|
-
|
|
127
|
-
config.providers = {
|
|
128
|
-
openai: {adapter: :openai, api_key: ENV.fetch("OPENAI_API_KEY")}
|
|
129
|
-
}
|
|
130
|
-
config.models = {
|
|
131
|
-
customer_support: {
|
|
132
|
-
target: "openai:gpt-5.6-luna",
|
|
133
|
-
settings: {temperature: 0.2}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
config.default_model = :customer_support
|
|
137
|
-
end
|
|
159
|
+
stream = CustomerSupportAgent.stream_ask("Can I get a refund?")
|
|
138
160
|
|
|
139
|
-
|
|
140
|
-
|
|
161
|
+
run = stream.each do |event|
|
|
162
|
+
case event.type
|
|
163
|
+
when :text_delta
|
|
164
|
+
print event.data.fetch(:text)
|
|
165
|
+
when :run_error
|
|
166
|
+
warn event.data.fetch(:message)
|
|
167
|
+
end
|
|
141
168
|
end
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
An agent may also keep a small amount of model-specific configuration beside its behavior:
|
|
145
169
|
|
|
146
|
-
|
|
147
|
-
class
|
|
148
|
-
model(
|
|
149
|
-
provider: "openai",
|
|
150
|
-
model: "gpt-5.6-luna",
|
|
151
|
-
reasoning_effort: "high"
|
|
152
|
-
)
|
|
153
|
-
end
|
|
170
|
+
puts "\n#{run.response}" if run.completed?
|
|
171
|
+
warn run.error.class.name if run.failed?
|
|
154
172
|
```
|
|
155
173
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
## Fit the agent into your application
|
|
174
|
+
The stream yields `LittleGhost::StreamEvent` values. Text, tool activity, usage, and completion all look the same across providers. When enumeration finishes, `.each` returns the same `LittleGhost::Run` that now holds the final outcome and response.
|
|
159
175
|
|
|
160
|
-
|
|
176
|
+
## Give the code a home
|
|
161
177
|
|
|
162
|
-
|
|
178
|
+
LittleGhost does not require an application layout. Keep definitions beside related application code, or use these optional conventions:
|
|
163
179
|
|
|
164
|
-
|
|
180
|
+
```text
|
|
181
|
+
app/
|
|
182
|
+
├── agents/
|
|
183
|
+
│ └── customer_support_agent.rb
|
|
184
|
+
├── assemblies/
|
|
185
|
+
│ └── response_workflow.rb
|
|
186
|
+
├── prompts/
|
|
187
|
+
│ └── customer_support/
|
|
188
|
+
│ └── system.erb
|
|
189
|
+
└── tools/
|
|
190
|
+
└── help_center_lookup_tool.rb
|
|
191
|
+
```
|
|
165
192
|
|
|
166
|
-
|
|
193
|
+
You now have the smallest useful LittleGhost application: one Agent, one Tool, and one familiar Ruby call.
|
|
167
194
|
|
|
168
|
-
|
|
195
|
+
When the feature grows, the calling style stays the same. An **assembly** lets one or more agents work as a unit while keeping `.ask` and `.stream_ask`. Read [Core Concepts](core_concepts.md) next and grow this Agent into a larger system.
|
|
@@ -0,0 +1,187 @@
|
|
|
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 leave the application for the selected external provider. Select providers from trusted configuration and account for their retention and data-residency policies.
|
|
43
|
+
|
|
44
|
+
## Configure once, call from anywhere
|
|
45
|
+
|
|
46
|
+
The `LittleGhost.configure` block above is the entire initializer. Controllers and jobs can call your Agent and Assembly classes directly.
|
|
47
|
+
|
|
48
|
+
Then call the Agent directly from a controller or job:
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
class SupportQuestionsController < ApplicationController
|
|
52
|
+
def create
|
|
53
|
+
run = CustomerSupportAgent.ask(
|
|
54
|
+
params.require(:question),
|
|
55
|
+
actor_id: current_user.id,
|
|
56
|
+
context: {account_id: current_user.account_id}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if run.completed?
|
|
60
|
+
render json: {answer: run.response}
|
|
61
|
+
else
|
|
62
|
+
render json: {error: "Support request failed"}, status: :bad_gateway
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
On the first class-level call, LittleGhost prepares model resolution, loading, prompt lookup, persistence, hooks, and factories. Later calls reuse those application services automatically.
|
|
69
|
+
|
|
70
|
+
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.
|
|
71
|
+
|
|
72
|
+
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.
|
|
73
|
+
|
|
74
|
+
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.
|
|
75
|
+
|
|
76
|
+
## Preserve conversation with Sessions
|
|
77
|
+
|
|
78
|
+
A **Session** lets one request continue an earlier conversation. Pass the same session ID and trusted actor ID with each related call:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
run = CustomerSupportAgent.ask(
|
|
82
|
+
"What did we decide about my refund?",
|
|
83
|
+
session_id: "conversation-42",
|
|
84
|
+
actor_id: authenticated_user.id
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
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.
|
|
89
|
+
|
|
90
|
+
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.
|
|
91
|
+
|
|
92
|
+
{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:
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
LittleGhost.configure do |config|
|
|
96
|
+
config.session_store = {
|
|
97
|
+
provider: LittleGhost::SessionStores::Filesystem,
|
|
98
|
+
root: "/var/lib/customer_support/sessions"
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
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.
|
|
104
|
+
|
|
105
|
+
## Stream or supervise long-running work
|
|
106
|
+
|
|
107
|
+
`.stream_ask` runs on the caller's thread and yields `StreamEvent` values as the answer arrives:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
stream = CustomerSupportAgent.stream_ask(question)
|
|
111
|
+
|
|
112
|
+
run = stream.each do |event|
|
|
113
|
+
publish(event) if event.type == :text_delta
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
record_outcome(run.outcome, error_type: run.error&.class&.name)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Use `start_execution` when the caller must stay free for other work, or when you want to deliver an interjection to an active response:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
execution = agent.start_execution(message: question) do |event|
|
|
123
|
+
event_buffer << event
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
execution.interject(message: "Include the latest ledger entry")
|
|
127
|
+
execution.wait(deadline: Time.now + 30)
|
|
128
|
+
execution.run.completed?
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
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.
|
|
132
|
+
|
|
133
|
+
## Treat tools as application boundaries
|
|
134
|
+
|
|
135
|
+
A tool schema checks the shape of model-supplied input. Your application still owns permission checks, safe retries, rate limits, tenant boundaries, and auditing.
|
|
136
|
+
|
|
137
|
+
Use the Tool binding's `run` to read current, application-established values from `run.invocation.context`. Do not make permission decisions from model arguments.
|
|
138
|
+
|
|
139
|
+
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`.
|
|
140
|
+
|
|
141
|
+
A `ToolError` message is visible to the model, so keep it safe to share. LittleGhost hides unexpected exception messages from model-facing results.
|
|
142
|
+
|
|
143
|
+
When a step retries, its tool calls may happen again too. Prefer read-only work, idempotency keys, or operations that are safe to repeat.
|
|
144
|
+
|
|
145
|
+
## Choose workspace and sandbox behavior explicitly
|
|
146
|
+
|
|
147
|
+
A Workspace gives one Run a place for files. A Sandbox decides how filesystem and process operations happen there.
|
|
148
|
+
|
|
149
|
+
`LittleGhost::UnrestrictedSandbox` uses the host machine with the Ruby process's permissions. It does not contain untrusted code. Expose only the tools the model needs, and use a real isolation boundary when untrusted code must run.
|
|
150
|
+
|
|
151
|
+
The Run closes workspaces and sandboxes that LittleGhost creates for it. If your application passes an existing instance instead, your application keeps ownership and must close it when its own lifecycle ends.
|
|
152
|
+
|
|
153
|
+
## Instrument without leaking the application
|
|
154
|
+
|
|
155
|
+
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.
|
|
156
|
+
|
|
157
|
+
An external telemetry service may receive application identifiers and event data. Redact sensitive values before they leave your boundary. Avoid attributes with many unique values, such as raw order or request IDs. Replacing one identifier does not make the rest of the data anonymous.
|
|
158
|
+
|
|
159
|
+
A composite `RunResult` includes short step summaries and trajectory queries. Keep detailed provider errors and sensitive diagnostics in trusted monitoring channels, not in model or user responses.
|
|
160
|
+
|
|
161
|
+
## Keep ownership and failure visible
|
|
162
|
+
|
|
163
|
+
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.
|
|
164
|
+
|
|
165
|
+
Ordinary execution failures appear on the Run and its final event. Cleanup, event delivery, or instrumentation can still raise an exception: once those boundaries fail, LittleGhost cannot promise a clean ending.
|
|
166
|
+
|
|
167
|
+
## Advanced: work with Runtime directly
|
|
168
|
+
|
|
169
|
+
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.
|
|
170
|
+
|
|
171
|
+
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:
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
configuration = LittleGhost::Configuration.new(root: isolated_root)
|
|
175
|
+
runtime = LittleGhost::Runtime.new(configuration: configuration)
|
|
176
|
+
agent = CustomerSupportAgent.new(runtime: runtime)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
An explicit Runtime is an independent configuration snapshot. It does not replace LittleGhost's shared default.
|
|
180
|
+
|
|
181
|
+
One Runtime can serve independent calls from several threads. Each call gets its own Run, participants, Tools, and Runtime-created workspace and sandbox. An Agent or Assembly already bound to an active Run must stay with that Run.
|
|
182
|
+
|
|
183
|
+
Within one SessionStore instance, LittleGhost serializes calls sharing a Session. Multi-process deployments need coordination from their store. Custom stores, identity and credential resolvers, model resolvers, hooks, instrumentation subscribers, providers, and resource factories may receive concurrent calls and must be thread-safe.
|
|
184
|
+
|
|
185
|
+
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.
|
|
186
|
+
|
|
187
|
+
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,132 @@
|
|
|
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 easy to scan. 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
|
+
## Override the convention when it helps
|
|
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`. Earlier roots win, which is useful when one trusted application layer overrides a shared prompt package.
|
|
110
|
+
|
|
111
|
+
## Treat views as application code
|
|
112
|
+
|
|
113
|
+
Prompt views run as ERB inside the Ruby process. They can call Ruby, so keep every prompt directory application-controlled and non-user-writable. Never turn a request or model-supplied path into a prompt root.
|
|
114
|
+
|
|
115
|
+
`TrustedPath` exists for the uncommon case where trusted application code selects a request-specific root. It records a trust decision; it does not make an untrusted directory safe.
|
|
116
|
+
|
|
117
|
+
## Keep request composition separate
|
|
118
|
+
|
|
119
|
+
A prompt view defines reusable instructions for one Agent. A Workflow may still build request-specific input for that Agent:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
invoke CustomerSupportAgent, input: <<~MESSAGE
|
|
123
|
+
#{input.text}
|
|
124
|
+
|
|
125
|
+
Verified research:
|
|
126
|
+
#{research}
|
|
127
|
+
MESSAGE
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The Workflow is composing this request. `CustomerSupportAgent` still receives its own system prompt view when it runs.
|
|
131
|
+
|
|
132
|
+
Continue with [Running in Production](production.md) to configure model roles, preserve sessions, supervise execution, and connect observability.
|
|
@@ -142,10 +142,10 @@ module LittleGhost
|
|
|
142
142
|
"little_ghost.model_retry",
|
|
143
143
|
source.data.merge(superseded_message_id:).compact
|
|
144
144
|
)
|
|
145
|
-
when :
|
|
145
|
+
when :agent_interjection_delivered
|
|
146
146
|
output << custom(
|
|
147
|
-
"little_ghost.
|
|
148
|
-
source.data.slice(:
|
|
147
|
+
"little_ghost.agent_interjection_delivered",
|
|
148
|
+
source.data.slice(:interjection_ids, :batch_key).compact
|
|
149
149
|
)
|
|
150
150
|
when :subagent
|
|
151
151
|
output << custom("little_ghost.subagent", source.data.fetch(:event, source.data))
|
|
@@ -10,7 +10,7 @@ module LittleGhost
|
|
|
10
10
|
# agent_as_tool SentimentAgent, name: "classify_sentiment"
|
|
11
11
|
# end
|
|
12
12
|
#
|
|
13
|
-
# The support model receives spawn, messaging,
|
|
13
|
+
# The support model receives spawn, messaging, interjection, waiting, and
|
|
14
14
|
# listing tools for the +research+ kind. It sees the sentiment agent as one
|
|
15
15
|
# regular tool whose result is returned to the current turn.
|
|
16
16
|
#
|