little_ghost 0.9.0 → 0.11.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 +20 -10
- data/docs/guides/assemblies.md +64 -12
- data/docs/guides/core_concepts.md +65 -61
- data/docs/guides/decisions.md +69 -0
- data/docs/guides/embeddings.md +23 -0
- data/docs/guides/getting_started.md +38 -68
- data/docs/guides/integrations.md +20 -6
- data/docs/guides/models_and_providers.md +10 -29
- data/docs/guides/production.md +17 -4
- data/docs/guides/providers.md +58 -11
- data/lib/little_ghost/ag_ui/adapter.rb +64 -1
- data/lib/little_ghost/agent.rb +1 -1
- data/lib/little_ghost/agent_stream_source.rb +41 -2
- data/lib/little_ghost/assembly.rb +22 -18
- data/lib/little_ghost/assembly_builder.rb +2 -2
- data/lib/little_ghost/assembly_execution.rb +3 -1
- data/lib/little_ghost/code_mode/broker.rb +5 -3
- data/lib/little_ghost/code_mode/javascript/client.rb +1 -1
- data/lib/little_ghost/code_mode/javascript/session.rb +22 -2
- data/lib/little_ghost/code_mode/ruby/session.rb +15 -1
- data/lib/little_ghost/code_mode/session.rb +3 -0
- data/lib/little_ghost/decision.rb +230 -0
- data/lib/little_ghost/execution.rb +3 -1
- data/lib/little_ghost/graph.rb +3 -1
- data/lib/little_ghost/model.rb +5 -0
- data/lib/little_ghost/model_operations.rb +14 -0
- data/lib/little_ghost/provider_registry.rb +1 -0
- data/lib/little_ghost/providers/base.rb +5 -0
- data/lib/little_ghost/providers/decision_http.rb +91 -0
- data/lib/little_ghost/providers/gemini.rb +34 -8
- data/lib/little_ghost/providers/open_router.rb +18 -1
- data/lib/little_ghost/providers/typesafe.rb +35 -0
- data/lib/little_ghost/run.rb +23 -8
- data/lib/little_ghost/run_context.rb +21 -0
- data/lib/little_ghost/runtime.rb +8 -3
- data/lib/little_ghost/stream_event.rb +5 -2
- data/lib/little_ghost/swarm.rb +14 -48
- data/lib/little_ghost/version.rb +1 -1
- data/lib/little_ghost/workflow.rb +74 -96
- data/lib/little_ghost.rb +5 -0
- metadata +6 -1
|
@@ -17,7 +17,8 @@ $ export OPENROUTER_API_KEY="..."
|
|
|
17
17
|
|
|
18
18
|
Use your application's secret manager outside a local shell, and never commit provider credentials.
|
|
19
19
|
|
|
20
|
-
This guide uses OpenRouter
|
|
20
|
+
This guide uses OpenRouter, a service that sends requests to your chosen AI
|
|
21
|
+
model. Set `OPENROUTER_API_KEY` to a key from your OpenRouter account. Prefer another hosted
|
|
21
22
|
provider or a local model server? Start with [Provider Support](providers.md).
|
|
22
23
|
|
|
23
24
|
## See your first answer
|
|
@@ -41,10 +42,11 @@ else
|
|
|
41
42
|
end
|
|
42
43
|
```
|
|
43
44
|
|
|
44
|
-
|
|
45
|
+
`model` selects the service and model to call. `system_prompt` supplies the
|
|
46
|
+
instructions the model follows on each request. Run the file with your bundle:
|
|
45
47
|
|
|
46
48
|
```sh
|
|
47
|
-
$ ruby customer_support_agent.rb
|
|
49
|
+
$ bundle exec ruby customer_support_agent.rb
|
|
48
50
|
```
|
|
49
51
|
|
|
50
52
|
`CustomerSupportAgent.ask` creates a `LittleGhost::Run` for this request. When the work finishes, the Run holds the outcome and response.
|
|
@@ -55,7 +57,9 @@ The selected external provider may receive system instructions, caller input, co
|
|
|
55
57
|
|
|
56
58
|
## Connect the agent to your application
|
|
57
59
|
|
|
58
|
-
The first agent can answer general questions. A **tool** gives it a focused
|
|
60
|
+
The first agent can answer general questions. A **tool** gives it a focused
|
|
61
|
+
operation backed by your Ruby code. Add this class after the `require` line,
|
|
62
|
+
before `CustomerSupportAgent`:
|
|
59
63
|
|
|
60
64
|
```ruby
|
|
61
65
|
class HelpCenterLookupTool < LittleGhost::Tool
|
|
@@ -80,7 +84,9 @@ class HelpCenterLookupTool < LittleGhost::Tool
|
|
|
80
84
|
end
|
|
81
85
|
```
|
|
82
86
|
|
|
83
|
-
|
|
87
|
+
`input_schema` describes the arguments the model may supply. Here, `topic` must
|
|
88
|
+
be one of the help center's keys. Replace the Agent definition and the call at
|
|
89
|
+
the end of the file with these:
|
|
84
90
|
|
|
85
91
|
```ruby
|
|
86
92
|
class CustomerSupportAgent < LittleGhost::Agent
|
|
@@ -106,84 +112,48 @@ LittleGhost checks the model's arguments before it calls
|
|
|
106
112
|
`HelpCenterLookupTool#call`. The Tool's result then becomes context for the
|
|
107
113
|
model.
|
|
108
114
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
While an Agent is working, LittleGhost binds each Tool instance to the current
|
|
116
|
-
Run. The Tool can read request values through its `run` accessor:
|
|
117
|
-
|
|
118
|
-
```ruby
|
|
119
|
-
class OrderStatusTool < LittleGhost::Tool
|
|
120
|
-
ORDER_STATUSES = {
|
|
121
|
-
["user-7", "account-2", "481"] => "out for delivery"
|
|
122
|
-
}.freeze
|
|
123
|
-
|
|
124
|
-
description "Look up an order that belongs to the current customer."
|
|
125
|
-
input_schema(
|
|
126
|
-
type: "object",
|
|
127
|
-
properties: {order_number: {type: "string"}},
|
|
128
|
-
required: ["order_number"],
|
|
129
|
-
additionalProperties: false
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
def call(input)
|
|
133
|
-
lookup = [
|
|
134
|
-
run.invocation.actor_id,
|
|
135
|
-
run.invocation.context.fetch("account_id"),
|
|
136
|
-
input.fetch("order_number")
|
|
137
|
-
]
|
|
138
|
-
|
|
139
|
-
ORDER_STATUSES.fetch(lookup) do
|
|
140
|
-
raise LittleGhost::ToolError, "Order not found"
|
|
141
|
-
end
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
class CustomerSupportAgent < LittleGhost::Agent
|
|
146
|
-
tools HelpCenterLookupTool, OrderStatusTool
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
run = CustomerSupportAgent.ask(
|
|
150
|
-
"Where is order 481?",
|
|
151
|
-
actor_id: "user-7",
|
|
152
|
-
context: {account_id: "account-2"}
|
|
153
|
-
)
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
Here, `order_number` came from the model. The application supplied `actor_id`
|
|
157
|
-
and `account_id` after authenticating the caller. LittleGhost places those
|
|
158
|
-
request values on `run.invocation`; context keys become strings.
|
|
159
|
-
|
|
160
|
-
> **Safety note:** Treat model-selected Tool arguments like any other external
|
|
161
|
-
> input. Check permission using the current user and account before returning
|
|
162
|
-
> private data or performing a write.
|
|
163
|
-
|
|
164
|
-
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.
|
|
115
|
+
> **Safety note:** The schema checks arguments, not permission. This example
|
|
116
|
+
> reads a public help center. Before a Tool returns private data or changes
|
|
117
|
+
> anything, check permission using the user and account identified by your
|
|
118
|
+
> application—not values supplied by the model. [Tools](tools.md) shows how to
|
|
119
|
+
> pass that information to a Tool.
|
|
165
120
|
|
|
166
121
|
## Stream the same agent
|
|
167
122
|
|
|
168
|
-
Use `.stream_ask` when a console, HTTP response, or user interface should receive
|
|
123
|
+
Use `.stream_ask` when a console, HTTP response, or user interface should receive
|
|
124
|
+
the answer as it is written. Replace the `.ask` call with the following code.
|
|
125
|
+
|
|
126
|
+
Each `:agent_stream` event contains the Agent's progress and a `source` that
|
|
127
|
+
identifies which Agent produced it. The source check below selects the Agent
|
|
128
|
+
you called directly: `/root` with no enclosing assembly steps. `:text_delta`
|
|
129
|
+
contains the next piece of its answer.
|
|
169
130
|
|
|
170
131
|
```ruby
|
|
171
132
|
stream = CustomerSupportAgent.stream_ask("Can I get a refund?")
|
|
172
133
|
|
|
173
134
|
run = stream.each do |event|
|
|
174
135
|
case event.type
|
|
175
|
-
when :
|
|
176
|
-
|
|
136
|
+
when :agent_stream
|
|
137
|
+
source = event.data.fetch(:source)
|
|
138
|
+
next unless source.agent_path == "/root" && source.assembly_path.empty?
|
|
139
|
+
|
|
140
|
+
progress = event.data.fetch(:event)
|
|
141
|
+
print progress.data.fetch(:text) if progress.type == :text_delta
|
|
177
142
|
when :run_error
|
|
178
143
|
warn event.data.fetch(:message)
|
|
179
144
|
end
|
|
180
145
|
end
|
|
181
146
|
|
|
182
|
-
|
|
147
|
+
run.response # The completed answer, separate from live progress.
|
|
183
148
|
warn run.error.class.name if run.failed?
|
|
184
149
|
```
|
|
185
150
|
|
|
186
|
-
|
|
151
|
+
When enumeration finishes, `.each` returns the Run with the final outcome and
|
|
152
|
+
complete response. You can display progress and still read the finished answer.
|
|
153
|
+
|
|
154
|
+
If you later add other Agents, their progress arrives in the same stream. Keep
|
|
155
|
+
the source check when your audience should see only this Agent's text. See
|
|
156
|
+
[Compose Agents](assemblies.md) to display several participants.
|
|
187
157
|
|
|
188
158
|
## Give the code a home
|
|
189
159
|
|
|
@@ -223,8 +193,8 @@ The source repository also contains a complete
|
|
|
223
193
|
[single-file Agent](https://github.com/littleghostai/little_ghost/tree/main/examples/basic_agent)
|
|
224
194
|
and a
|
|
225
195
|
[coding harness](https://github.com/littleghostai/little_ghost/tree/main/examples/coding_harness)
|
|
226
|
-
that
|
|
227
|
-
|
|
196
|
+
that shows a larger application with several agents and tools for working
|
|
197
|
+
with files.
|
|
228
198
|
|
|
229
199
|
You now have the smallest useful LittleGhost application: one Agent, one Tool, and one familiar Ruby call.
|
|
230
200
|
|
data/docs/guides/integrations.md
CHANGED
|
@@ -27,10 +27,22 @@ events = LittleGhost::AGUI::Adapter.new.stream(
|
|
|
27
27
|
events.each { |event| websocket.write(JSON.generate(event)) }
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
The adapter translates
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
The adapter translates Run lifecycle and final outcomes, plus selected Agent
|
|
31
|
+
progress such as model output, Tool activity, and retries. By default, it selects
|
|
32
|
+
only the root Agent: `agent_path == "/root"` with an empty `assembly_path`.
|
|
33
|
+
Nested Agent progress is not forwarded. The adapter reads Agent progress from
|
|
34
|
+
`:agent_stream` and reports aggregate usage separately.
|
|
35
|
+
|
|
36
|
+
Pass `source_filter:` to `LittleGhost::AGUI::Adapter.new` to select other
|
|
37
|
+
participants. This application-owned callable receives an `AgentStreamSource`
|
|
38
|
+
and returns a truthy value only for sources the client may see. Match the
|
|
39
|
+
enclosing assembly path as well as the Agent identity when selecting a nested
|
|
40
|
+
participant. Computed assembly answers arrive in `RUN_FINISHED`'s result, not
|
|
41
|
+
as synthetic text deltas.
|
|
42
|
+
|
|
43
|
+
The adapter does not keep state between calls. Your application owns the
|
|
44
|
+
connection, backpressure, disconnect behavior, and any request state its
|
|
45
|
+
callbacks need.
|
|
34
46
|
|
|
35
47
|
LittleGhost may emit event types beyond the core AG-UI set. Decide whether the
|
|
36
48
|
client preserves or ignores types it does not recognize. See the [AG-UI event
|
|
@@ -38,8 +50,10 @@ documentation](https://docs.ag-ui.com/concepts/events) when implementing the
|
|
|
38
50
|
client.
|
|
39
51
|
|
|
40
52
|
> **Safety note:** A Run stream can include model output, Tool arguments and
|
|
41
|
-
> results, errors, and participant activity.
|
|
42
|
-
>
|
|
53
|
+
> results, errors, and participant activity. Source selection controls which
|
|
54
|
+
> Agent progress is forwarded; it does not redact that progress or filter the
|
|
55
|
+
> final Run result. Check that the connected user may see those values, and
|
|
56
|
+
> filter sensitive fields before sending or storing events.
|
|
43
57
|
|
|
44
58
|
Calling `each` drives the source stream on the caller's fiber or thread. When a
|
|
45
59
|
client disconnects, stop enumerating and decide whether the application should
|
|
@@ -74,9 +74,10 @@ application obtains short-lived credentials at runtime, configure a credential
|
|
|
74
74
|
resolver that returns them for the selected connection.
|
|
75
75
|
|
|
76
76
|
> **Safety note:** The selected provider may receive system instructions,
|
|
77
|
-
> caller input,
|
|
78
|
-
>
|
|
79
|
-
> provider endpoints under
|
|
77
|
+
> caller input, application state, question text, conversation
|
|
78
|
+
> history, Tool results, schemas, and attachments. Choose a provider that is
|
|
79
|
+
> appropriate for that data, and keep credentials and provider endpoints under
|
|
80
|
+
> application control.
|
|
80
81
|
|
|
81
82
|
## Choose a role for each request
|
|
82
83
|
|
|
@@ -155,29 +156,9 @@ Results and Content](structured_outputs_and_content.md) covers strict schemas,
|
|
|
155
156
|
provider strategies, repair behavior, and the application checks that still
|
|
156
157
|
belong outside the schema.
|
|
157
158
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
response = LittleGhost.embed(
|
|
165
|
-
model: "primary:openai/text-embedding-3-small",
|
|
166
|
-
inputs: ["Reset a password", "Track a transfer"]
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
response.vectors.length # => 2
|
|
170
|
-
response.dimensions
|
|
171
|
-
response.usage.input_tokens
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
The response keeps vectors in the same order as the inputs. LittleGhost rejects
|
|
175
|
-
an incomplete or malformed response instead of returning a partial batch.
|
|
176
|
-
Embedding text is sent to the selected provider, so choose one appropriate for
|
|
177
|
-
that data. [Provider Support](providers.md) identifies adapters with embedding
|
|
178
|
-
support; `LittleGhost::Embeddings::Request` documents settings and request
|
|
179
|
-
bounds.
|
|
180
|
-
|
|
181
|
-
Continue with [Prompts as Views](prompt_views.md) when an Agent's instructions
|
|
182
|
-
outgrow one string. See [Structured Results and Content](structured_outputs_and_content.md)
|
|
183
|
-
when you need checked result shapes, images, or documents.
|
|
159
|
+
Continue with [Decisions](decisions.md) for typed, structured answers or
|
|
160
|
+
[Embeddings](embeddings.md) for text representations. [Prompts as
|
|
161
|
+
Views](prompt_views.md) moves growing Agent instructions into conventional ERB
|
|
162
|
+
files. See [Structured Results and
|
|
163
|
+
Content](structured_outputs_and_content.md) for checked result shapes, images,
|
|
164
|
+
or documents.
|
data/docs/guides/production.md
CHANGED
|
@@ -218,19 +218,32 @@ values as the answer arrives:
|
|
|
218
218
|
stream = CustomerSupportAgent.stream_ask(question)
|
|
219
219
|
|
|
220
220
|
run = stream.each do |event|
|
|
221
|
-
|
|
221
|
+
next unless event.type == :agent_stream
|
|
222
|
+
|
|
223
|
+
source = event.data.fetch(:source)
|
|
224
|
+
next unless source.agent_path == "/root" && source.assembly_path.empty?
|
|
225
|
+
|
|
226
|
+
progress = event.data.fetch(:event)
|
|
227
|
+
publish(progress) if progress.type == :text_delta
|
|
222
228
|
end
|
|
223
229
|
|
|
224
230
|
record_outcome(run.outcome, error_type: run.error&.class&.name)
|
|
225
231
|
```
|
|
226
232
|
|
|
227
|
-
|
|
233
|
+
Every Run includes live source-tagged `:agent_stream` events from all its Agents,
|
|
234
|
+
including nested assemblies and subagents. This example displays only root Agent
|
|
235
|
+
text. The same contract applies to `start_execution`. Progress is not repeated
|
|
236
|
+
as raw events or replayed when a Workflow selects a result. Read `run.result`
|
|
237
|
+
or the terminal Run event for the complete answer and aggregate usage. Pass
|
|
238
|
+
`include_agent_events: false` to omit all Agent progress without changing work,
|
|
239
|
+
lifecycle events, or final results.
|
|
228
240
|
|
|
229
241
|
> **Safety note:** Contextual events can include inputs, reasoning, Tool
|
|
230
242
|
> arguments and results, errors, and output from every participant. Check that
|
|
231
243
|
> the destination may see the complete Run, or filter the events before sending
|
|
232
|
-
> or storing them. LittleGhost's AG-UI adapter
|
|
233
|
-
>
|
|
244
|
+
> or storing them. LittleGhost's [AG-UI adapter](integrations.md) selects root
|
|
245
|
+
> Agent progress by default; its `source_filter:` option lets the application
|
|
246
|
+
> select permitted assembly participants.
|
|
234
247
|
|
|
235
248
|
Use `start_execution` when the caller must stay free for other work, or when you want to deliver an interjection to an active response:
|
|
236
249
|
|
data/docs/guides/providers.md
CHANGED
|
@@ -23,17 +23,24 @@ end
|
|
|
23
23
|
protocol. An Agent can now use a target such as
|
|
24
24
|
`primary:openai/gpt-5.6-luna`.
|
|
25
25
|
|
|
26
|
-
| Provider or endpoint | Adapter | Generation | Embeddings |
|
|
27
|
-
| --- | --- | --- | --- |
|
|
28
|
-
| OpenRouter | `:openrouter` | Chat Completions | When the model supports it |
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
|
|
|
32
|
-
|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
26
|
+
| Provider or endpoint | Adapter | Generation | Decisions | Embeddings |
|
|
27
|
+
| --- | --- | --- | --- | --- |
|
|
28
|
+
| OpenRouter | `:openrouter` | Chat Completions | Typed decision endpoints | When the model supports it |
|
|
29
|
+
| TypeSafe | `:typesafe` | No | System One decisions | No |
|
|
30
|
+
| OpenAI | `:openai` | Responses or Chat Completions | No | Yes |
|
|
31
|
+
| Compatible API | `:openai_compatible` | Responses or Chat Completions | No | When the endpoint implements it |
|
|
32
|
+
| Ollama | `:openai_compatible` | Responses or Chat Completions | No | When the model supports it |
|
|
33
|
+
| Anthropic | `:anthropic` | Messages | No | No |
|
|
34
|
+
| Gemini | `:gemini` | Gemini API | No | No |
|
|
35
|
+
| Vertex AI | `:vertex_ai` | Gemini on Vertex AI | No | No |
|
|
36
|
+
| Bedrock | `:bedrock` | Converse | No | Titan Text Embeddings V2 |
|
|
37
|
+
| LM Studio | `:lm_studio` | Responses or Chat Completions | No | Yes |
|
|
38
|
+
|
|
39
|
+
TypeSafe Jev is a decision model that returns structured answers to typed
|
|
40
|
+
questions. Its API and OpenRouter's Jev routes support the same question
|
|
41
|
+
types.
|
|
42
|
+
These operations return structured answers directly and do not create an Agent
|
|
43
|
+
Run. See [Decisions](decisions.md) for the question and answer model.
|
|
37
44
|
|
|
38
45
|
Capabilities can vary by model, account, and server version. Handle provider
|
|
39
46
|
failures even when a model is expected to support a feature.
|
|
@@ -60,6 +67,46 @@ Use OpenRouter's `publisher/model` form, such as
|
|
|
60
67
|
The selected model must support embeddings before you call
|
|
61
68
|
`LittleGhost.embed`.
|
|
62
69
|
|
|
70
|
+
For Jev decisions, OpenRouter supports both decision endpoints. The default
|
|
71
|
+
uses the Decisions API and namespaced model identifiers such as
|
|
72
|
+
`openrouter:~typesafe/jev-latest`:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
LittleGhost.configure do |config|
|
|
76
|
+
config.providers = {
|
|
77
|
+
openrouter: {
|
|
78
|
+
adapter: :openrouter,
|
|
79
|
+
api_key: ENV.fetch("OPENROUTER_API_KEY"),
|
|
80
|
+
decision_api: :decisions
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Set `decision_api: :system_one` to use `/api/v1/systemone`; that route
|
|
87
|
+
accepts a bare TypeSafe model identifier such as `jev-latest`, used as
|
|
88
|
+
`openrouter:jev-latest`.
|
|
89
|
+
OpenRouter describes these routes in its [Jev guide](https://openrouter.ai/blog/insights/what-is-jev/).
|
|
90
|
+
|
|
91
|
+
### TypeSafe
|
|
92
|
+
|
|
93
|
+
Connect directly to TypeSafe's System One endpoint with a TypeSafe API key:
|
|
94
|
+
|
|
95
|
+
```ruby
|
|
96
|
+
LittleGhost.configure do |config|
|
|
97
|
+
config.providers = {
|
|
98
|
+
typesafe: {
|
|
99
|
+
adapter: :typesafe,
|
|
100
|
+
api_key: ENV.fetch("TYPESAFE_API_KEY")
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Use a Jev identifier such as `typesafe:jev-latest`. This adapter supports
|
|
107
|
+
typed decisions only. See the [TypeSafe API reference](https://docs.typesafe.ai/api)
|
|
108
|
+
for question and answer fields.
|
|
109
|
+
|
|
63
110
|
### OpenAI
|
|
64
111
|
|
|
65
112
|
```ruby
|
|
@@ -18,8 +18,27 @@ module LittleGhost
|
|
|
18
18
|
# The adapter has no state between #stream calls, so one instance can
|
|
19
19
|
# translate independent runs.
|
|
20
20
|
#
|
|
21
|
+
# Progress comes from the Run's native +:agent_stream+ events. By default,
|
|
22
|
+
# only the top-level Agent's progress reaches the interface: its
|
|
23
|
+
# AgentStreamSource[rdoc-ref:LittleGhost::AgentStreamSource] has +agent_path+
|
|
24
|
+
# <tt>/root</tt> and an empty +assembly_path+. A composite run's final
|
|
25
|
+
# response arrives in +RUN_FINISHED+ independently of participant progress.
|
|
26
|
+
# Unwrapped progress and wrapped invocation usage or terminal events are
|
|
27
|
+
# ignored; run lifecycle and aggregate usage remain separate.
|
|
28
|
+
#
|
|
21
29
|
# === Choose what the interface receives
|
|
22
30
|
#
|
|
31
|
+
# Pass a +source_filter+ callable to select a participant whose progress is
|
|
32
|
+
# intended for the destination. The callable receives an AgentStreamSource
|
|
33
|
+
# and replaces the default root selection. Choose it in trusted application
|
|
34
|
+
# code, not from request or model values.
|
|
35
|
+
#
|
|
36
|
+
# adapter = LittleGhost::AGUI::Adapter.new(source_filter: lambda { |source|
|
|
37
|
+
# step = source.assembly_path.first
|
|
38
|
+
# source.agent_path == "/root" && source.assembly_path.length == 1 &&
|
|
39
|
+
# step.assembly_id == "response_workflow" && step.participant == "response"
|
|
40
|
+
# })
|
|
41
|
+
#
|
|
23
42
|
# Provider plaintext reasoning becomes AG-UI reasoning events. Tool
|
|
24
43
|
# arguments and results, invocation metadata, subagent events, trace context,
|
|
25
44
|
# and selected error text also pass through without redaction. Authorize and
|
|
@@ -28,8 +47,32 @@ module LittleGhost
|
|
|
28
47
|
# continuity artifacts are never exposed here.
|
|
29
48
|
class Adapter
|
|
30
49
|
TERMINAL_EVENTS = %i[run_partial run_cancel run_stop run_error].freeze # :nodoc:
|
|
50
|
+
PROGRESS_EVENTS = %i[
|
|
51
|
+
message_start reasoning_delta text_delta message_stop
|
|
52
|
+
tool_call_start tool_call_delta tool_call_stop tool_stop
|
|
53
|
+
model_retry agent_interjection_delivered
|
|
54
|
+
].freeze # :nodoc:
|
|
55
|
+
|
|
56
|
+
# Selects which Agent's progress reaches the interface.
|
|
57
|
+
#
|
|
58
|
+
# +source_filter+ is a callable receiving an AgentStreamSource and returning
|
|
59
|
+
# a truthy value to include its progress. Omit it to include only a root
|
|
60
|
+
# Agent outside any composite assembly. A shared adapter may call the filter
|
|
61
|
+
# concurrently for independent streams, so keep its captured state safe for
|
|
62
|
+
# concurrent use. Filter exceptions propagate to the caller of #stream.
|
|
63
|
+
# Raises ArgumentError when +source_filter+ is not callable.
|
|
64
|
+
def initialize(source_filter: nil)
|
|
65
|
+
if !source_filter.nil? && !source_filter.respond_to?(:call)
|
|
66
|
+
raise ArgumentError, "source_filter must be callable"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
@source_filter = source_filter
|
|
70
|
+
end
|
|
31
71
|
|
|
32
|
-
# Lazily translates +events+
|
|
72
|
+
# Lazily translates a Run's +events+ into AG-UI hashes for one interface run.
|
|
73
|
+
#
|
|
74
|
+
# Returns an Enumerator. Selected Agent progress arrives as it is produced;
|
|
75
|
+
# +RUN_FINISHED+ carries the run's final response in <tt>result[:response]</tt>.
|
|
33
76
|
def stream(events, thread_id:, run_id:)
|
|
34
77
|
Enumerator.new do |output|
|
|
35
78
|
message_id = nil
|
|
@@ -39,6 +82,9 @@ module LittleGhost
|
|
|
39
82
|
tool_call_ids = {}
|
|
40
83
|
|
|
41
84
|
events.each do |source|
|
|
85
|
+
source = selected_event(source)
|
|
86
|
+
next unless source
|
|
87
|
+
|
|
42
88
|
superseded_message_id = message_id if source.type == :model_retry && message_started
|
|
43
89
|
if reasoning_id && source.type != :reasoning_delta
|
|
44
90
|
output << event("REASONING_MESSAGE_END", messageId: reasoning_message_id)
|
|
@@ -182,6 +228,23 @@ module LittleGhost
|
|
|
182
228
|
|
|
183
229
|
private
|
|
184
230
|
|
|
231
|
+
def selected_event(event)
|
|
232
|
+
return unless event.is_a?(StreamEvent)
|
|
233
|
+
return PROGRESS_EVENTS.include?(event.type) ? nil : event unless event.type == :agent_stream
|
|
234
|
+
|
|
235
|
+
source = event.data[:source]
|
|
236
|
+
progress = event.data[:event]
|
|
237
|
+
return unless source.is_a?(AgentStreamSource) && progress.is_a?(StreamEvent)
|
|
238
|
+
return unless PROGRESS_EVENTS.include?(progress.type)
|
|
239
|
+
|
|
240
|
+
selected = if @source_filter
|
|
241
|
+
@source_filter.call(source)
|
|
242
|
+
else
|
|
243
|
+
source.agent_path == "/root" && source.assembly_path.empty?
|
|
244
|
+
end
|
|
245
|
+
progress if selected
|
|
246
|
+
end
|
|
247
|
+
|
|
185
248
|
def event(type, **attributes)
|
|
186
249
|
{type:, **attributes.compact}
|
|
187
250
|
end
|
data/lib/little_ghost/agent.rb
CHANGED
|
@@ -2327,7 +2327,7 @@ module LittleGhost
|
|
|
2327
2327
|
agent_path:,
|
|
2328
2328
|
operation_id:,
|
|
2329
2329
|
parent_operation_id:,
|
|
2330
|
-
assembly_path: agent_stream_path
|
|
2330
|
+
assembly_path: AgentStreamScope.path(run:, path: agent_stream_path)
|
|
2331
2331
|
)
|
|
2332
2332
|
AgentStreamSink.new(destination: events, run:, source:, input:)
|
|
2333
2333
|
end
|
|
@@ -29,7 +29,8 @@ module LittleGhost
|
|
|
29
29
|
|
|
30
30
|
##
|
|
31
31
|
# :attr_reader: assembly_kind
|
|
32
|
-
# The +:workflow+, +:swarm+, +:graph+, or custom assembly kind.
|
|
32
|
+
# The +:workflow+, +:swarm+, +:graph+, +:tool+, or custom assembly kind.
|
|
33
|
+
# A +:tool+ step marks an assembly invoked through a Tool.
|
|
33
34
|
|
|
34
35
|
##
|
|
35
36
|
# :attr_reader: participant
|
|
@@ -67,7 +68,11 @@ module LittleGhost
|
|
|
67
68
|
# A +:agent_stream+ StreamEvent carries this value in +data[:source]+ and the
|
|
68
69
|
# Agent's event as a separate frozen StreamEvent in +data[:event]+.
|
|
69
70
|
# +assembly_path+ is empty for a top-level Agent and contains one
|
|
70
|
-
# AgentStreamStep for each enclosing composite assembly participant
|
|
71
|
+
# AgentStreamStep for each enclosing composite assembly participant or
|
|
72
|
+
# assembly-as-tool boundary. When an Agent invokes the Tool, the boundary's
|
|
73
|
+
# +assembly_id+ identifies that Agent and +participant+ names the Tool. Tool
|
|
74
|
+
# boundaries do not change the delegated Agent's authorization or
|
|
75
|
+
# managed-subagent path.
|
|
71
76
|
class AgentStreamSource < Data # :doc:
|
|
72
77
|
##
|
|
73
78
|
# :attr_reader: agent_id
|
|
@@ -238,7 +243,41 @@ module LittleGhost
|
|
|
238
243
|
end
|
|
239
244
|
end
|
|
240
245
|
|
|
246
|
+
module AgentStreamScope # :nodoc: all
|
|
247
|
+
module_function
|
|
248
|
+
|
|
249
|
+
def with_tool(target:, name:)
|
|
250
|
+
return yield unless target.run
|
|
251
|
+
|
|
252
|
+
execution = ExecutionState[:tool_execution]
|
|
253
|
+
source = execution.events.source if execution&.events.is_a?(AgentStreamSink)
|
|
254
|
+
prefix = target.agent_stream_path
|
|
255
|
+
parent_path = source&.assembly_path || path(run: target.run, path: prefix)
|
|
256
|
+
boundary = AgentStreamStep.build(
|
|
257
|
+
assembly_id: source&.agent_id || target.class.assembly_id,
|
|
258
|
+
assembly_kind: :tool,
|
|
259
|
+
participant: name,
|
|
260
|
+
step_id: execution&.operation_id || SecureRandom.uuid
|
|
261
|
+
)
|
|
262
|
+
# Prebuilt descendant factories inherit this scope without mutating a
|
|
263
|
+
# reusable Agent's bound path or its authorization identity.
|
|
264
|
+
scope = {run: target.run, prefix:, path: (parent_path + [boundary]).freeze}.freeze
|
|
265
|
+
ExecutionState.with(agent_stream_tool_scope: scope) { yield }
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def path(run:, path:)
|
|
269
|
+
scope = ExecutionState[:agent_stream_tool_scope]
|
|
270
|
+
return path unless run && scope && scope.fetch(:run).equal?(run)
|
|
271
|
+
|
|
272
|
+
prefix = scope.fetch(:prefix)
|
|
273
|
+
suffix = (path.take(prefix.length) == prefix) ? path.drop(prefix.length) : path
|
|
274
|
+
scope.fetch(:path) + suffix
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
241
278
|
class AgentStreamSink # :nodoc: all
|
|
279
|
+
attr_reader :source
|
|
280
|
+
|
|
242
281
|
def initialize(destination:, run:, source:, input:)
|
|
243
282
|
@destination = destination
|
|
244
283
|
@run = run
|
|
@@ -58,11 +58,12 @@ module LittleGhost
|
|
|
58
58
|
#
|
|
59
59
|
# Enumeration yields StreamEvent objects and returns the terminal Run.
|
|
60
60
|
# The same Invocation fields accepted by .ask may be supplied as +options+.
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
# and
|
|
64
|
-
#
|
|
65
|
-
#
|
|
61
|
+
# Agent progress arrives once through +:agent_stream+, with source metadata
|
|
62
|
+
# identifying every Agent in the Run, including nested participants.
|
|
63
|
+
# Lifecycle and terminal results remain separate events. Filter sources
|
|
64
|
+
# before forwarding progress to a destination that may see only some
|
|
65
|
+
# participants. Set <tt>include_agent_events: false</tt> to omit all Agent
|
|
66
|
+
# progress without changing execution or final results. See Run.
|
|
66
67
|
def stream_ask(message, **options)
|
|
67
68
|
snapshot = definition
|
|
68
69
|
stream = nil
|
|
@@ -177,7 +178,7 @@ module LittleGhost
|
|
|
177
178
|
end
|
|
178
179
|
|
|
179
180
|
# Builds the top-level Run used by a standalone assembly.
|
|
180
|
-
def build_run(payload = nil,
|
|
181
|
+
def build_run(payload = nil, **payload_options) # :nodoc:
|
|
181
182
|
payload = entrypoint_payload(payload, payload_options) unless payload_options.empty?
|
|
182
183
|
payload = payload.dup if payload.is_a?(Hash)
|
|
183
184
|
cancellation_token = if payload.is_a?(Hash)
|
|
@@ -191,14 +192,14 @@ module LittleGhost
|
|
|
191
192
|
options[:cancellation_token] = cancellation_token if cancellation_token
|
|
192
193
|
options[:workspace] = workspace if workspace
|
|
193
194
|
options[:sandbox] = sandbox if sandbox
|
|
194
|
-
options[:include_agent_events_by_default] = true if include_agent_events_by_default
|
|
195
195
|
runtime.build_run(payload, **options)
|
|
196
196
|
end
|
|
197
197
|
|
|
198
198
|
# Starts +payload+ in the background and returns an Execution.
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
#
|
|
199
|
+
# The consumer receives source-tagged +:agent_stream+ progress from every
|
|
200
|
+
# Agent in the Run, including nested participants. Filter sources for the
|
|
201
|
+
# intended destination. Set +include_agent_events+ to +false+ in +payload+
|
|
202
|
+
# to omit Agent progress while keeping lifecycle and terminal results.
|
|
202
203
|
def start_execution(payload, &event_consumer)
|
|
203
204
|
ensure_standalone!
|
|
204
205
|
Execution.start(build_stream_run(payload), &event_consumer)
|
|
@@ -230,10 +231,10 @@ module LittleGhost
|
|
|
230
231
|
#
|
|
231
232
|
# A standalone stream returns its terminal Run after enumeration. A
|
|
232
233
|
# run-scoped stream finishes with an +invocation_stop+ event containing its
|
|
233
|
-
# RunResult.
|
|
234
|
-
#
|
|
235
|
-
# <tt>include_agent_events: false</tt> to omit
|
|
236
|
-
#
|
|
234
|
+
# RunResult. Standalone streams include source-tagged +:agent_stream+ events
|
|
235
|
+
# from every Agent in the Run by default. Applications filter sources before
|
|
236
|
+
# forwarding progress, or set <tt>include_agent_events: false</tt> to omit
|
|
237
|
+
# all Agent progress. This option does not change execution or final results.
|
|
237
238
|
def stream_ask(message, **options)
|
|
238
239
|
if standalone?
|
|
239
240
|
options[:deadline_at] = options.delete(:deadline) if options.key?(:deadline)
|
|
@@ -259,6 +260,9 @@ module LittleGhost
|
|
|
259
260
|
# A directly bound Agent remains the target for every call, so overlapping
|
|
260
261
|
# calls raise AgentBusyError. Class-level Agent#agent_as_tool declarations
|
|
261
262
|
# build a fresh Agent per context-free call.
|
|
263
|
+
# Within the same Run, progress from the target and its descendants carries a
|
|
264
|
+
# +:tool+ boundary in AgentStreamSource#assembly_path. It cannot be mistaken
|
|
265
|
+
# for root Agent progress, including when this instance is reused across calls.
|
|
262
266
|
def as_tool(name: self.class.assembly_id, description: self.class.description, preserve_context: false)
|
|
263
267
|
build_tool(name:, description:, preserve_context:, factory: nil)
|
|
264
268
|
end
|
|
@@ -321,7 +325,9 @@ module LittleGhost
|
|
|
321
325
|
options[:interjection_metadata] = context&.interjection_metadata
|
|
322
326
|
options[:interjection_ids] = context&.interjection_ids || []
|
|
323
327
|
end
|
|
324
|
-
result =
|
|
328
|
+
result = AgentStreamScope.with_tool(target:, name:) do
|
|
329
|
+
target.call(input.fetch("input"), **options)
|
|
330
|
+
end
|
|
325
331
|
if result.is_a?(Run)
|
|
326
332
|
raise result.error if result.error
|
|
327
333
|
|
|
@@ -406,9 +412,7 @@ module LittleGhost
|
|
|
406
412
|
private
|
|
407
413
|
|
|
408
414
|
def build_stream_run(payload) # :nodoc:
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
build_run(payload, include_agent_events_by_default: true)
|
|
415
|
+
build_run(payload)
|
|
412
416
|
end
|
|
413
417
|
|
|
414
418
|
def run_entrypoint_class # :nodoc:
|