little_ghost 0.2.1 → 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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +72 -74
  3. data/docs/guides/assemblies.md +286 -0
  4. data/docs/guides/core_concepts.md +159 -135
  5. data/docs/guides/getting_started.md +114 -83
  6. data/docs/guides/production.md +187 -0
  7. data/docs/guides/prompt_views.md +132 -0
  8. data/lib/little_ghost/ag_ui/adapter.rb +3 -3
  9. data/lib/little_ghost/agent/delegation.rb +35 -8
  10. data/lib/little_ghost/agent/tool_loop.rb +2 -1
  11. data/lib/little_ghost/agent.rb +280 -326
  12. data/lib/little_ghost/agent_builder.rb +20 -4
  13. data/lib/little_ghost/agent_factory.rb +3 -0
  14. data/lib/little_ghost/{agent_interruptions.rb → agent_interjections.rb} +12 -12
  15. data/lib/little_ghost/assembly.rb +345 -0
  16. data/lib/little_ghost/assembly_builder.rb +497 -0
  17. data/lib/little_ghost/assembly_execution.rb +535 -0
  18. data/lib/little_ghost/configuration.rb +263 -39
  19. data/lib/little_ghost/content.rb +5 -5
  20. data/lib/little_ghost/data_map.rb +209 -0
  21. data/lib/little_ghost/errors.rb +10 -2
  22. data/lib/little_ghost/execution.rb +206 -0
  23. data/lib/little_ghost/graph.rb +930 -0
  24. data/lib/little_ghost/message.rb +4 -4
  25. data/lib/little_ghost/model_resolver.rb +2 -2
  26. data/lib/little_ghost/prompt_resolver.rb +2 -0
  27. data/lib/little_ghost/run.rb +190 -64
  28. data/lib/little_ghost/run_context.rb +33 -20
  29. data/lib/little_ghost/run_result.rb +22 -11
  30. data/lib/little_ghost/runtime/hook.rb +9 -4
  31. data/lib/little_ghost/runtime.rb +134 -36
  32. data/lib/little_ghost/sandbox.rb +1 -1
  33. data/lib/little_ghost/session.rb +12 -23
  34. data/lib/little_ghost/session_store.rb +9 -5
  35. data/lib/little_ghost/session_stores/agent_core_memory.rb +64 -56
  36. data/lib/little_ghost/session_stores/filesystem.rb +261 -0
  37. data/lib/little_ghost/session_stores/memory.rb +7 -0
  38. data/lib/little_ghost/subagents/manager.rb +42 -42
  39. data/lib/little_ghost/support/executor.rb +14 -2
  40. data/lib/little_ghost/support/loader.rb +2 -2
  41. data/lib/little_ghost/support.rb +15 -3
  42. data/lib/little_ghost/swarm.rb +439 -0
  43. data/lib/little_ghost/tool.rb +88 -20
  44. data/lib/little_ghost/tools/write_todos.rb +6 -1
  45. data/lib/little_ghost/tracing/open_telemetry.rb +14 -3
  46. data/lib/little_ghost/unrestricted_sandbox.rb +1 -1
  47. data/lib/little_ghost/version.rb +1 -1
  48. data/lib/little_ghost/workflow.rb +224 -90
  49. data/lib/little_ghost.rb +36 -25
  50. metadata +17 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 450a61dddcb991278311cf48d3a86b65fa09930a939ad4cf877b226cf59be74b
4
- data.tar.gz: ee6d950747b8b1ef19078c4e6c18252eb47cb8581def86efa266bf208703683c
3
+ metadata.gz: a2136594414b7f1c810eba51718916646506a4816ced53cf088034e091a0af5c
4
+ data.tar.gz: a7a2a279680b64f936096f70772374b3dcfc1ba21dfb2e428da91be22d385af4
5
5
  SHA512:
6
- metadata.gz: 0c726c4cae4d584facab0c7542f6f95b939ac6c8bac78ff3b5d525d5e47ce673280d773572243b61df14da8881b4105488beffa45b4659abffb40f11e71368ad
7
- data.tar.gz: bc7d5f909e3d48382b889ad4d4cf8fadd2df7978b09069a20f7632da858f788d6210eb1a13921b232a1a5ebf8d25cebfa240e4c4090ef96d135df2d74f376e5a
6
+ metadata.gz: 3c78c760b8b9be5bb9aa3394fffdcc0735173366826b263534421c9755f0b0f458b4d14f75c65bee76bb8da839beb67917e6ce222d438073bef95f9258c23ec0
7
+ data.tar.gz: 6effec94de30f7b4e1ed5fcad776cd9579da8645a95ca510ebe5ee5a26921ded2c34eae8f50b70295e326c6874135cd7293de202874980da957ca73558852fe9
data/README.md CHANGED
@@ -1,18 +1,46 @@
1
- # Build AI features with LittleGhost
1
+ # Build AI features that feel at home in Ruby
2
2
 
3
- <hr>
3
+ LittleGhost is a Ruby library for building AI features with agents and composable assemblies. With `OPENROUTER_API_KEY` set, start with one class, give it a prompt, and call it like the rest of your application code:
4
4
 
5
- **Growing in public.** LittleGhost is under active development, and interfaces may evolve between releases. Pin the gem version and review the release notes when upgrading.
5
+ ```ruby
6
+ require "little_ghost"
7
+
8
+ class CustomerSupportAgent < LittleGhost::Agent
9
+ model "openrouter:openai/gpt-5.6-luna"
10
+ system_prompt "Answer customer questions clearly and concisely."
11
+ end
6
12
 
7
- <hr>
13
+ run = CustomerSupportAgent.ask("Draft a friendly greeting for a customer.")
14
+ run.response
15
+ # One possible response: Hi! How can I help today?
16
+ ```
17
+
18
+ That small definition is already a complete agent. LittleGhost makes the model call, tracks usage, supports streaming, and closes the resources it creates for the request. Add a tool when the agent needs something from your application. Bring in more agents when the work grows.
19
+
20
+ Model requests may send system instructions, caller input, conversation history, tool results, and attachments to the selected external provider. Model wording can vary between runs. Choose providers and the data you send them with the same care as any other external service.
8
21
 
9
- Bring agents and agentic workflows into an existing Ruby system, or use them as the core of a dedicated AI service. LittleGhost connects model providers, tools, streaming, sessions, delegation, deterministic workflows, and observability through Ruby APIs.
22
+ ## Install the gem
10
23
 
11
- Set `OPENAI_API_KEY`, then paste this customer support agent into a Ruby console or file. It selects a model directly and exposes one validated application tool:
24
+ LittleGhost requires Ruby 3.3 or newer. Add it to your bundle and provide a provider credential:
12
25
 
13
26
  ```ruby
14
- require "little_ghost"
27
+ gem "little_ghost"
28
+ ```
29
+
30
+ ```sh
31
+ $ bundle install
32
+ $ export OPENROUTER_API_KEY="..."
33
+ ```
34
+
35
+ OpenRouter keeps the first setup to one credential. It is not required: LittleGhost also includes adapters for OpenAI-compatible APIs, Anthropic, Gemini, Vertex AI, and Bedrock. [Running in Production](docs/guides/production.md) shows how to configure providers and give model choices application-facing names.
15
36
 
37
+ LittleGhost runs inside your Ruby process. Use it from a controller, job, CLI, or service. If you want a conventional layout, start with `app/agents`, `app/assemblies`, `app/prompts`, and `app/tools`.
38
+
39
+ ## Give an agent real capabilities
40
+
41
+ Tools let an agent call focused parts of your application:
42
+
43
+ ```ruby
16
44
  class HelpCenterLookupTool < LittleGhost::Tool
17
45
  description "Look up a help center entry by topic."
18
46
  input_schema(
@@ -29,97 +57,67 @@ class HelpCenterLookupTool < LittleGhost::Tool
29
57
  end
30
58
 
31
59
  class CustomerSupportAgent < LittleGhost::Agent
32
- description "Answers customer support questions."
33
- model "openai:gpt-5.6-luna"
34
- system_prompt "Answer clearly. Check the help center before stating company guidance."
60
+ model "openrouter:openai/gpt-5.6-luna"
61
+ system_prompt "Check the help center before stating company guidance."
35
62
  tools HelpCenterLookupTool
36
63
  end
37
-
38
- run = CustomerSupportAgent.ask("Can I get a refund after two weeks?")
39
- puts run.response
40
- # One possible response:
41
- # Refunds are available within 30 days, so your purchase is eligible.
42
- ```
43
-
44
- The completed `LittleGhost::Run` exposes its outcome, final response, normalized messages, token usage, and terminal error. Streaming callers receive `LittleGhost::StreamEvent` objects instead of provider-specific payloads:
45
-
46
- ```ruby
47
- CustomerSupportAgent.stream_ask("Can I get a refund?").each do |event|
48
- print event.data.fetch(:text) if event.type == :text_delta
49
- end
50
64
  ```
51
65
 
52
- ## How the pieces fit
53
-
54
- ```text
55
- provider connections + model selections ──> ModelResolver ──> provider
56
-
57
- request ──> CustomerSupportAgent ──> HelpCenterLookupTool
58
-
59
- └────────> ResearchAgent subagent
66
+ The schema checks the shape of the input. Your Ruby code still decides whether the operation is allowed and safe. The result goes back to the model as context.
60
67
 
61
- request ──> ResponseWorkflow ──> ResearchAgent ──> CustomerSupportAgent
62
- ```
68
+ ## Grow without changing the caller
63
69
 
64
- Configuration owns shared services such as model resolution, sessions, lookup paths, workspaces, sandboxes, and instrumentation. Agent classes own behavior: their model selection, prompt, tools, limits, result schema, and delegation policy. Tools expose narrow application operations. A subagent lets the model delegate within configured limits; a workflow uses ordinary Ruby when the application must control ordering.
65
-
66
- Each top-level execution owns a run lifecycle. The run checkpoints session state, closes its resources, aggregates usage, and emits framework events whether the entrypoint is an agent or a workflow.
67
-
68
- ## Installation and configuration
69
-
70
- LittleGhost requires Ruby 3.3 or newer. Add it to your bundle:
70
+ An **agent** owns one model loop. An **assembly** is one or more agents working as a unit. You call either one the same way:
71
71
 
72
72
  ```ruby
73
- gem "little_ghost"
73
+ CustomerSupportAgent.ask(question)
74
+ ResponseWorkflow.ask(question)
75
+ ProblemSolverSwarm.ask(question)
76
+ SupportFlowGraph.ask(question)
74
77
  ```
75
78
 
76
- Then run `bundle install`. Built-in OpenAI-compatible, OpenRouter, Anthropic, Gemini, Vertex AI, and Amazon Bedrock integrations use Ruby's standard library and normalize responses into the same protocol.
79
+ Choose the coordination style that matches who should control the next step:
77
80
 
78
- Configuration does not require a particular directory layout. Provider connections and model profiles resolve independently in this order:
81
+ - A **subagent** lets a model delegate an addressable task.
82
+ - A **workflow** uses ordinary Ruby for ordering and branching.
83
+ - A **swarm** lets configured agents choose permitted handoffs.
84
+ - A **graph** makes allowed routes explicit as nodes and edges.
79
85
 
80
- 1. An inline `config.providers` or `config.models` declaration.
81
- 2. The corresponding explicit `config.providers_path` or `config.models_path`.
82
- 3. The optional conventional file under `config/little_ghost/`.
83
- 4. Environment-based provider selection and the built-in `default` profile.
86
+ A Workflow or Graph can contain agents, other assemblies, or both. Named classes are the clearest place to begin. Builders are there when your application discovers the participants or routes at runtime.
84
87
 
85
- An explicit path must exist. A missing conventional file is valid. The conventional form keeps connection policy separate from model roles:
88
+ ```text
89
+ request ──> CustomerSupportAgent
86
90
 
87
- ```yaml
88
- # config/little_ghost/providers.yml
89
- providers:
90
- openai:
91
- adapter: openai
92
- api_key: <%= ENV.fetch("OPENAI_API_KEY") %>
93
- ```
91
+ request ──> ResponseWorkflow ──> ResearchAgent ──> CustomerSupportAgent
94
92
 
95
- ```yaml
96
- # config/little_ghost/models.yml
97
- default_model: customer_support
98
- models:
99
- customer_support:
100
- target: openai:gpt-5.6-luna
101
- ```
93
+ request ──> ProblemSolverSwarm ──> TriageAgent ──handoff──> BillingAgent
102
94
 
103
- By default, LittleGhost maps `default` to GPT-5.6 Luna. It configures conventional OpenRouter and OpenAI connections from nonblank `LITTLEGHOST_OPENROUTER_API_KEY`, `LITTLEGHOST_OPENAI_API_KEY`, `OPENROUTER_API_KEY`, and `OPENAI_API_KEY` values; that order determines the default when more than one provider is available. Model inputs—including prompts, history, tool data, and attachments—leave the application for the selected external provider. Configure providers explicitly when provider choice or data residency matters.
95
+ request ──> SupportFlowGraph ──> TriageAgent ──edge──> ResponseAgent
96
+ ```
104
97
 
105
- Applications that need custom routing can subclass `LittleGhost::ModelResolver` and install the class with `config.model_resolver`. A custom resolver owns its profiles and default role; configuring `models`, `models_path`, or `default_model` at the same time is an error. Provider configuration remains available to the resolver.
98
+ The result stays familiar too. Every call returns a `Run` with the response,
99
+ outcome, usage, and any final error. A coordinated assembly also records which
100
+ participants ran. Use `.stream_ask` to watch the work as it happens.
106
101
 
107
- LittleGhost runs inside the surrounding Ruby process; it does not prescribe an HTTP server, CLI, job system, or application layout. `config/little_ghost`, `app/agents`, `app/prompts`, `app/tools`, and `app/skills` are optional conventions. Every path can be configured, and agents, prompts, and tools may live wherever the application loads them.
102
+ **Growing in public.** LittleGhost is under active development, and interfaces may evolve between releases. Pin the gem version and review release notes when upgrading.
108
103
 
109
- ## Documentation
104
+ ## Keep going
110
105
 
111
- - [Getting Started](docs/guides/getting_started.md) builds and streams the customer support example.
112
- - [Core Concepts](docs/guides/core_concepts.md) explains models, agents, tools, runs, delegation, workflows, and sessions.
113
- - [API reference](rdoc-ref:LittleGhost) covers exact signatures, options, and lifecycle behavior.
106
+ - [Getting Started](docs/guides/getting_started.md) takes you from installation to a tool-backed, streaming agent.
107
+ - [Core Concepts](docs/guides/core_concepts.md) builds the mental model from Agent to Assembly.
108
+ - [Compose Agents](docs/guides/assemblies.md) walks through workflows, swarms, graphs, nesting, and builders.
109
+ - [Prompts as Views](docs/guides/prompt_views.md) gives growing instructions, shared pieces, and application values a natural home.
110
+ - [Running in Production](docs/guides/production.md) covers configuration, sessions, execution, observability, and trust boundaries.
111
+ - [API reference](rdoc-ref:LittleGhost) provides exact signatures and lifecycle contracts.
114
112
 
115
- ## Contributing
113
+ ### For contributors
116
114
 
117
115
  See the [contributing guide](https://github.com/mattyr/little_ghost/blob/main/CONTRIBUTING.md), [Code of Conduct](https://github.com/mattyr/little_ghost/blob/main/CODE_OF_CONDUCT.md), and [security policy](https://github.com/mattyr/little_ghost/blob/main/SECURITY.md).
118
116
 
119
117
  ```sh
120
- bundle install
121
- bundle exec rake test
122
- bundle exec standardrb --no-fix
118
+ $ bundle install
119
+ $ bundle exec rake test
120
+ $ bundle exec standardrb --no-fix
123
121
  ```
124
122
 
125
- LittleGhost is licensed under the MIT License.
123
+ LittleGhost is available under the MIT License.
@@ -0,0 +1,286 @@
1
+ # Compose Agents
2
+
3
+ An Assembly lets several participants answer through the same familiar calls as one Agent. This guide grows the customer-support example through each coordination style, then shows how to nest and construct assemblies dynamically.
4
+
5
+ ## Start with the shared contract
6
+
7
+ Callers do not need a branch for each implementation:
8
+
9
+ ```ruby
10
+ entrypoint = urgent? ? EscalationWorkflow : CustomerSupportAgent
11
+ run = entrypoint.ask(question)
12
+ ```
13
+
14
+ `Agent`, `Workflow`, `Swarm`, and `Graph` all answer through the Assembly calling style. They differ in how they coordinate work, not in how your application calls them.
15
+
16
+ ## Use a Workflow for explicit application logic
17
+
18
+ A Workflow's `perform` method is ordinary Ruby. Inside it, `invoke` prepares a child call. Read `.output` when you need an intermediate answer. Return the final `invoke` call untouched so its response can stream to the caller.
19
+
20
+ ```ruby
21
+ class ResponseWorkflow < LittleGhost::Workflow
22
+ private
23
+
24
+ def perform
25
+ research = invoke(ResearchAgent).output
26
+
27
+ invoke CustomerSupportAgent, input: <<~PROMPT
28
+ #{input.text}
29
+
30
+ Verified research:
31
+ #{research}
32
+ PROMPT
33
+ end
34
+ end
35
+
36
+ run = ResponseWorkflow.ask("Why is transfer 481 pending?")
37
+ run.response
38
+ ```
39
+
40
+ Every participant passed to `invoke` can be an Agent or another Assembly. By default, each child receives the caller's history and application context. Pass `history: []`, `context: {}`, or redacted values when a child should see less.
41
+
42
+ Each child Agent keeps its own [prompt view](prompt_views.md). The Workflow supplies request-specific input; it does not replace that Agent's reusable system instructions.
43
+
44
+ The last child is special because its events become the Workflow's public stream. Return that `invoke` without consuming it:
45
+
46
+ ```ruby
47
+ # Wrong: this returns a String after consuming the final invocation.
48
+ def perform
49
+ invoke(CustomerSupportAgent).output
50
+ end
51
+
52
+ # Right: this returns the lazy invocation itself.
53
+ def perform
54
+ invoke CustomerSupportAgent
55
+ end
56
+ ```
57
+
58
+ The first version produces a failed top-level Run whose error is `ProtocolError`. Use `.output` only when Ruby needs an intermediate answer before choosing the next step.
59
+
60
+ ### Choose a branch in Ruby
61
+
62
+ Each branch should end with its final unconsumed invocation:
63
+
64
+ ```ruby
65
+ class RoutedResponseWorkflow < LittleGhost::Workflow
66
+ private
67
+
68
+ def perform
69
+ route = invoke(TriageAgent, as: :triage).output
70
+
71
+ if route == "billing"
72
+ invoke BillingAgent, as: :billing_response
73
+ else
74
+ invoke CustomerSupportAgent, as: :general_response
75
+ end
76
+ end
77
+ end
78
+ ```
79
+
80
+ `as:` gives the child a readable participant name in steps, trajectories, and telemetry. It does not change which Assembly runs.
81
+
82
+ ### Run independent work in parallel
83
+
84
+ Use `parallel` when several inputs can be processed independently:
85
+
86
+ ```ruby
87
+ class InvestigationWorkflow < LittleGhost::Workflow
88
+ private
89
+
90
+ def perform
91
+ findings = parallel(
92
+ invoke(LedgerResearchAgent),
93
+ invoke(PolicyResearchAgent),
94
+ max_concurrency: 2
95
+ )
96
+
97
+ invoke CustomerSupportAgent, input: <<~PROMPT
98
+ #{input.text}
99
+
100
+ Findings:
101
+ #{findings.join("\n")}
102
+ PROMPT
103
+ end
104
+ end
105
+ ```
106
+
107
+ `max_concurrency` limits how many calls run at once. Each one gets its own copy of the workflow context. Cancellation still depends on the provider or tool noticing its token or deadline.
108
+
109
+ ## Use a Swarm for specialist handoffs
110
+
111
+ A Swarm keeps one Agent active at a time. You decide which specialists it may hand work to:
112
+
113
+ ```ruby
114
+ class ProblemSolverSwarm < LittleGhost::Swarm
115
+ member TriageAgent
116
+ member BillingAgent
117
+ member AccountAgent
118
+
119
+ start TriageAgent
120
+ handoff TriageAgent, to: [BillingAgent, AccountAgent]
121
+ handoff BillingAgent, to: TriageAgent
122
+ handoff AccountAgent, to: TriageAgent
123
+
124
+ max_steps 10
125
+ max_handoff_repeats 2
126
+ end
127
+ ```
128
+
129
+ The active model sees a handoff tool listing the members it may choose next. LittleGhost accepts only the routes you declared. `max_steps` limits total member executions. `max_handoff_repeats` limits how often the same directed handoff, such as triage to billing, may repeat.
130
+
131
+ Swarm members must be Agents, so each transition stays a direct model-to-model handoff. Caller history and application context are opt-in for each member. Handoff messages come from a model; never treat them as permission to read data or perform an action.
132
+
133
+ Opt in only for a member that needs the data:
134
+
135
+ ```ruby
136
+ member AccountAgent, history: true, context: true
137
+ ```
138
+
139
+ Intermediate model text stays out of the caller-facing stream, leaving one coherent public answer. This is not a privacy boundary. The next member receives the handoff, and the result keeps a bounded summary of the journey.
140
+
141
+ ## Use a Graph for guided routes
142
+
143
+ A Graph names the possible stops and the routes between them. Start with a conditional route before adding parallel branches:
144
+
145
+ ```ruby
146
+ class SupportFlowGraph < LittleGhost::Graph
147
+ node :triage, TriageAgent
148
+ node :billing, BillingAgent
149
+ node :general, CustomerSupportAgent
150
+ node :respond, CustomerSupportAgent
151
+
152
+ start :triage
153
+
154
+ edge :triage, :billing do |state|
155
+ state.result(:triage).output == "billing"
156
+ end
157
+ edge :triage, :general
158
+ edge :billing, :respond
159
+ edge :general, :respond
160
+ finish :respond
161
+ end
162
+
163
+ SupportFlowGraph.validate!
164
+ ```
165
+
166
+ Conditions and input mappers read an immutable `Graph::State`. At most one conditional edge may match. If several match, LittleGhost raises `AssemblyRoutingError` instead of guessing which one wins. One unconditional edge can catch the request when none match.
167
+
168
+ Graph nodes start without caller history or application context. They still receive the original input or the output routed from an earlier node. Map or redact that data before it moves to a provider or participant that should see less.
169
+
170
+ Opt in for a trusted node when it needs caller context:
171
+
172
+ ```ruby
173
+ node :account_lookup, AccountLookupAgent, context: true
174
+ ```
175
+
176
+ ### Fork and join bounded parallel paths
177
+
178
+ Use a fork when one result should start several independent branches. A join brings their answers back together:
179
+
180
+ ```ruby
181
+ class InvestigationGraph < LittleGhost::Graph
182
+ node :triage, TriageAgent
183
+ node :ledger, LedgerResearchAgent
184
+ node :policy, PolicyResearchAgent
185
+ node :respond, CustomerSupportAgent
186
+
187
+ start :triage
188
+ fork :triage, to: [:ledger, :policy], max_concurrency: 2
189
+ join(
190
+ [:ledger, :policy],
191
+ to: :respond,
192
+ input: ->(state) { state.branch_results.transform_values(&:output) }
193
+ )
194
+ finish :respond
195
+ end
196
+ ```
197
+
198
+ An error edge can send an expected failure to a recovery Assembly. Call `validate!` before the first run.
199
+
200
+ Once the topology grows, `InvestigationGraph.to_mermaid` returns Mermaid diagram source for the routes you declared. Render it in a Mermaid-aware editor or documentation page when a picture makes the graph easier to review.
201
+
202
+ ## Make retries safe
203
+
204
+ Workflow calls, Swarm members, and Graph nodes can set timeouts and retries. Use them for work that can safely be attempted again:
205
+
206
+ ```ruby
207
+ invoke(
208
+ ResearchAgent,
209
+ timeout: 15,
210
+ retries: 2,
211
+ retry_on: [LittleGhost::ProviderError],
212
+ retry_delay: 0.25
213
+ )
214
+ ```
215
+
216
+ A timeout asks the running code to stop; it cannot forcibly end arbitrary Ruby or provider work. A retry repeats the whole child step. Retry only selected failures, and make sure repeated external actions are safe.
217
+
218
+ Retries start at zero. When `retries` is greater than zero, `retry_on` must list the exception classes that are safe to try again. LittleGhost does not retry every failure by default.
219
+
220
+ ## Inspect what the assembly did
221
+
222
+ A composite result remembers the steps it took. `trajectory` lets you explore them:
223
+
224
+ ```ruby
225
+ run = InvestigationGraph.ask("Why is transfer 481 pending?")
226
+ trajectory = run.result.trajectory
227
+
228
+ trajectory.each { |step| puts "#{step.participant}: #{step.status}" }
229
+ trajectory.transitions
230
+ ledger = trajectory.find { |step| step.participant == "ledger" }
231
+ policy = trajectory.find { |step| step.participant == "policy" }
232
+ trajectory.concurrent?(ledger.id, policy.id)
233
+ ```
234
+
235
+ Step outputs and buffered events have size limits. Use your application's instrumentation when trusted operators need deeper diagnostics.
236
+
237
+ ## Compose assemblies inside assemblies
238
+
239
+ Workflow and Graph participants accept any Assembly definition:
240
+
241
+ ```ruby
242
+ class ResolutionGraph < LittleGhost::Graph
243
+ node :investigate, InvestigationWorkflow
244
+ node :resolve, ProblemSolverSwarm
245
+
246
+ start :investigate
247
+ edge :investigate, :resolve
248
+ finish :resolve
249
+ end
250
+ ```
251
+
252
+ An Assembly can also become an Agent tool:
253
+
254
+ ```ruby
255
+ class SupportCoordinatorAgent < LittleGhost::Agent
256
+ assembly_as_tool InvestigationGraph,
257
+ name: "investigate_support_request",
258
+ preserve_context: false
259
+ end
260
+ ```
261
+
262
+ The nested assembly receives the parent Tool's current working state. That state may include values restored from a Session. `preserve_context` controls conversation history only: when it is false, working state still passes to the nested assembly. Any nested Tool doing privileged work must authorize with values the application established for the current request or checked again after loading.
263
+
264
+ ## Reach for builders when definitions are dynamic
265
+
266
+ Classes are the preferred form in application code. Use a builder when trusted application configuration decides the nodes or routes:
267
+
268
+ ```ruby
269
+ graph = LittleGhost::GraphBuilder.new(
270
+ id: "support_flow",
271
+ description: "Routes customer support requests"
272
+ )
273
+
274
+ graph.node :triage, TriageAgent
275
+ graph.node :respond, CustomerSupportAgent
276
+ graph.start :triage
277
+ graph.edge :triage, :respond
278
+ graph.finish :respond
279
+ graph.validate!
280
+
281
+ run = graph.ask("Where is my order?")
282
+ ```
283
+
284
+ Each builder uses the same declarations as its matching class. The builder stays editable, but each run gets a fixed copy of its current definition. Later edits affect later runs. Ruby callbacks still see any application objects they captured.
285
+
286
+ Continue with [Prompts as Views](prompt_views.md) to give each Agent's growing instructions and shared prompt pieces a natural home.