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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +68 -84
  3. data/docs/guides/assemblies.md +286 -0
  4. data/docs/guides/core_concepts.md +126 -231
  5. data/docs/guides/getting_started.md +114 -87
  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 +1 -1
  10. data/lib/little_ghost/agent.rb +167 -172
  11. data/lib/little_ghost/{agent_interruptions.rb → agent_interjections.rb} +12 -12
  12. data/lib/little_ghost/assembly.rb +55 -21
  13. data/lib/little_ghost/assembly_builder.rb +40 -2
  14. data/lib/little_ghost/assembly_execution.rb +87 -4
  15. data/lib/little_ghost/configuration.rb +263 -39
  16. data/lib/little_ghost/content.rb +5 -5
  17. data/lib/little_ghost/data_map.rb +209 -0
  18. data/lib/little_ghost/errors.rb +2 -2
  19. data/lib/little_ghost/execution.rb +32 -32
  20. data/lib/little_ghost/graph.rb +22 -3
  21. data/lib/little_ghost/message.rb +4 -4
  22. data/lib/little_ghost/model_resolver.rb +2 -2
  23. data/lib/little_ghost/prompt_resolver.rb +2 -0
  24. data/lib/little_ghost/run.rb +87 -49
  25. data/lib/little_ghost/run_context.rb +33 -20
  26. data/lib/little_ghost/runtime/hook.rb +3 -3
  27. data/lib/little_ghost/runtime.rb +71 -31
  28. data/lib/little_ghost/session.rb +12 -23
  29. data/lib/little_ghost/session_store.rb +9 -5
  30. data/lib/little_ghost/session_stores/agent_core_memory.rb +64 -56
  31. data/lib/little_ghost/session_stores/filesystem.rb +261 -0
  32. data/lib/little_ghost/session_stores/memory.rb +7 -0
  33. data/lib/little_ghost/subagents/manager.rb +42 -42
  34. data/lib/little_ghost/swarm.rb +13 -5
  35. data/lib/little_ghost/tool.rb +56 -14
  36. data/lib/little_ghost/tools/write_todos.rb +6 -1
  37. data/lib/little_ghost/tracing/open_telemetry.rb +1 -1
  38. data/lib/little_ghost/version.rb +1 -1
  39. data/lib/little_ghost/workflow.rb +30 -21
  40. data/lib/little_ghost.rb +29 -25
  41. metadata +7 -2
@@ -1,182 +1,156 @@
1
1
  # Core Concepts
2
2
 
3
- Start with one agent. In LittleGhost, an **agent** is a reusable Ruby definition for one model loop: it selects a model, supplies instructions, exposes tools, and decides when the model has finished answering one request.
4
-
5
- ```text
6
- shared configuration
7
- └── ModelResolver ── resolves model selections ──> provider clients
8
-
9
- one request
10
- └── Run
11
- ├── CustomerSupportAgent
12
- │ ├── HelpCenterLookupTool
13
- │ └── ResearchAgent subagent (model-directed)
14
- └── sessions, resources, usage, events, and terminal result
15
- ```
16
-
17
- The sections below build outward from that unit. After the agent, the guide introduces its model, tools, and run lifecycle. It then names an **assembly**: anything a caller can invoke like one agent, including coordinated workflows, swarms, and graphs.
18
-
19
- ## Agents declare one model-driven behavior
20
-
21
- An agent class keeps the behavior for one application role together:
3
+ Build one model-driven behavior in a Ruby class, then call it like Ruby. That is the idea LittleGhost grows from.
22
4
 
23
5
  ```ruby
24
6
  class CustomerSupportAgent < LittleGhost::Agent
25
- description "Answers customer support questions."
26
- model :customer_support
27
- system_prompt "Answer clearly. Check the help center before stating company guidance."
7
+ model "openrouter:openai/gpt-5.6-luna"
8
+ system_prompt "Answer customer questions clearly."
28
9
  tools HelpCenterLookupTool
29
10
  end
30
- ```
31
11
 
32
- The class-level DSL is inheritable. It can declare prompts, limits, callbacks, tool classes, structured results, context management, skills, and delegation. Capabilities remain inactive until their corresponding DSL is called.
12
+ run = CustomerSupportAgent.ask("Where is my order?")
13
+ run.response
14
+ ```
33
15
 
34
- `CustomerSupportAgent.ask` creates a standalone entrypoint, consumes one `LittleGhost::Run`, and returns that run. `CustomerSupportAgent.stream_ask` creates the same entrypoint and yields events while it works. Create `CustomerSupportAgent.new(runtime:)` explicitly when several calls should reuse one runtime.
16
+ From there, add only what the work needs. Give the agent a tool. Let it ask a specialist for help. Or coordinate several agents while the rest of your application keeps making the same call.
35
17
 
36
- ```ruby
37
- run = CustomerSupportAgent.ask("Can I get a refund?")
38
- run.response # final text from the top-level execution
39
- run.result.output # text, or a validated structured value when declared
40
- ```
18
+ ## An Agent owns one model loop
41
19
 
42
- ## Models can be selected directly or by role
20
+ An **Agent** defines one model-driven behavior. It chooses the model, supplies the instructions and tools, and carries one request through to an answer.
43
21
 
44
- An agent can name a canonical target directly when the choice belongs beside its behavior:
22
+ The class holds the behavior you want to reuse. Each call brings its own input, history, context, settings, and attachments. Request data never needs to live on the class.
45
23
 
46
- ```ruby
47
- class CustomerSupportAgent < LittleGhost::Agent
48
- model "openai:gpt-5.6-luna"
49
- end
24
+ ```text
25
+ CustomerSupportAgent
26
+ ├── model selection
27
+ ├── system prompt
28
+ ├── HelpCenterLookupTool
29
+ └── limits and optional capabilities
50
30
  ```
51
31
 
52
- It can also attach trusted model settings without defining a shared profile:
32
+ An Agent can return text or checked, structured data. Later, you can add streaming, sessions, or callbacks. None of them are required to begin.
53
33
 
54
- ```ruby
55
- class DeliberateSupportAgent < LittleGhost::Agent
56
- model(provider: "openai", model: "gpt-5.6-luna", reasoning_effort: "high")
57
- end
58
- ```
34
+ ## A Tool connects the model to Ruby
59
35
 
60
- In both forms, `provider` is the name of a configured connection. A role such as `customer_support` adds stable application vocabulary when several agents or deployments should share routing policy:
36
+ A **Tool** is one focused thing an agent can ask your application to do. It has a name, a description, an input schema, and the Ruby code that does the work.
61
37
 
62
38
  ```ruby
63
- LittleGhost.configure do |config|
64
- config.providers = {
65
- openai: {adapter: :openai, api_key: ENV.fetch("OPENAI_API_KEY")}
66
- }
67
- config.models = {
68
- customer_support: {target: "openai:gpt-5.6-luna"}
69
- }
70
- end
71
- ```
39
+ class HelpCenterLookupTool < LittleGhost::Tool
40
+ description "Look up a help center entry by topic."
41
+ input_schema(
42
+ type: "object",
43
+ properties: {topic: {type: "string"}},
44
+ required: ["topic"],
45
+ additionalProperties: false
46
+ )
72
47
 
73
- ```ruby
74
- class CustomerSupportAgent < LittleGhost::Agent
75
- model :customer_support
48
+ def call(input)
49
+ {"refunds" => "Refunds are available within 30 days."}
50
+ .fetch(input.fetch("topic"))
51
+ end
76
52
  end
77
53
  ```
78
54
 
79
- Strings and symbols without a colon are roles; strings containing a colon are canonical targets; mappings require `provider` and `model`, with remaining keys treated as model settings. Role names cannot contain a colon. Direct targets and mappings bypass role inheritance and overlays.
55
+ LittleGhost checks the model's arguments, calls the tool, and gives the result back to the model. The schema checks shape, not permission. Authorize sensitive reads and actions inside the tool with trusted application context.
80
56
 
81
- Dotted roles inherit from the nearest registered parent. `ResearchAgent` can request `customer_support.research` and initially use the `customer_support` profile; registering `customer_support.research` later specializes it. A resolver caller may pass an explicit `profiles:` overlay without mutating the configured profiles or agent class. Because an overlay can select a different registered provider, model, and settings, it is trusted application configuration and must be constructed or allowlisted by the application rather than copied from unchecked request data. The base resolver does not inspect application-specific invocation fields.
57
+ ## A Run owns one top-level execution
82
58
 
83
- The provider performs model I/O. `LittleGhost::ModelResolver` resolves application intent into a `LittleGhost::Model`, which carries the provider, target, settings, details, and role for a run.
59
+ Every `.ask` or `.stream_ask` creates a **Run**. Think of it as the record of one trip through LittleGhost. It opens what the request needs, records how the work ended, and closes the resources it owns.
84
60
 
85
- ## Runs own top-level lifecycle
61
+ ```ruby
62
+ run = CustomerSupportAgent.ask("Where is order 481?")
86
63
 
87
- A `LittleGhost::Run` owns one top-level execution. It opens the session, workspace, sandbox, agent entrypoint, and registered resources, then closes owned resources in reverse order. Later sections show how the same lifecycle can own a coordinated entrypoint.
64
+ run.completed? # => true
65
+ run.response
66
+ # One possible response: Order 481 is out for delivery.
67
+ run.usage # => normalized token usage
68
+ run.result # => the complete LittleGhost::RunResult
69
+ ```
88
70
 
89
- The run is both executable and enumerable. `#call` consumes it; `#each` streams `LittleGhost::StreamEvent` objects. After termination, the run reports one outcome: completed, failed, partial at a deadline, or cancelled. It also exposes the final response, result, usage, and error.
71
+ The Agent defines reusable behavior; the Run records what happened this time.
90
72
 
91
- Long-lived services can supervise a run without making their request thread own its execution:
73
+ ### Follow one request
92
74
 
93
- ```ruby
94
- execution = CustomerSupportAgent.new.start_execution(
95
- message: "Investigate transfer 481"
96
- ) do |event|
97
- event_buffer << event
98
- end
75
+ One Run owns the trip from request to result:
99
76
 
100
- execution.interrupt_response(message: "Include the latest ledger entry")
101
- run = execution.wait(deadline: Time.now + 30)
77
+ ```text
78
+ Run
79
+ ├── Invocation: caller input, history, and application context
80
+ ├── RunContext: mutable working state for this execution
81
+ └── Agent and Tools ──> RunResult
102
82
  ```
103
83
 
104
- `LittleGhost::Execution` owns the worker, preserves request-scoped execution state, and coordinates cancellation, interruptions, waiting, and bounded shutdown. The underlying run still owns agent resources and its terminal outcome. Event consumers run on the worker thread. Applications should keep them thread-safe and avoid blocking indefinitely.
84
+ An **Invocation** is the request in LittleGhost's standard shape. Its `context` contains current request values supplied by your application. A Tool can read those values through `run.invocation.context` when it authorizes work.
105
85
 
106
- An `Invocation` is the request envelope. It normalizes the current message and history, generates missing identifiers, and retains application-specific fields with indifferent string and symbol keys. Caller identity remains explicit. If session persistence needs tenant isolation, derive its actor from trusted authentication state; never trust a model-supplied or unverified request field.
86
+ The **RunContext** carries mutable working state in `context.state`. At the top level, saved Session state is loaded first, then current Invocation context is added. Child Assemblies may receive a copy, a mapped value, or no context at all. Recheck saved values before using them for permission decisions.
107
87
 
108
- ## Tools are validated application boundaries
88
+ A Tool's **Binding** gives the Tool access to objects created for this run, including the Agent, Run, workspace, and sandbox. These objects are separate from the arguments chosen by the model.
109
89
 
110
- `HelpCenterLookupTool` exposes exactly one operation to the model:
90
+ The final **RunResult** keeps the complete assembly result. Its `text` is the final text answer. Its `output` returns structured data when the Agent declared a result schema, and text otherwise. The top-level `Run#response` is always the caller-facing text.
111
91
 
112
- ```ruby
113
- class HelpCenterLookupTool < LittleGhost::Tool
114
- description "Look up a help center entry by topic."
115
- input_schema(
116
- type: "object",
117
- properties: {topic: {type: "string"}},
118
- required: ["topic"],
119
- additionalProperties: false
120
- )
92
+ ### See how a call ended
121
93
 
122
- def call(input)
123
- HelpCenterRepository.fetch(input.fetch("topic"))
124
- end
125
- end
126
- ```
127
-
128
- LittleGhost validates the model's input before invoking `#call`. Hashes and arrays returned by a tool are JSON-encoded; other values become text. Expected application failures can raise `LittleGhost::ToolError`; unexpected exception messages are sanitized before they reach model context.
94
+ Top-level calls normally return a Run, even when execution fails. The terminal event carries the same outcome when you stream:
129
95
 
130
- A tool can return a `LittleGhost::Tool::ExecutionResult` with `companion_content` when the next model request also needs text, images, or documents. LittleGhost keeps the ordinary tool result intact, then appends each tool's companion blocks as a transient user message in tool-call order. Session persistence omits those transient messages. Tool-use, tool-result, and reasoning blocks are rejected as companion content.
96
+ | What happened | Run outcome | Terminal event | What Ruby does |
97
+ | --- | --- | --- | --- |
98
+ | The assembly completed | `completed` | `:run_stop` | Returns the Run |
99
+ | Model, provider, or assembly execution failed | `failed` | `:run_error` | Returns the Run; inspect `run.error` |
100
+ | The deadline stopped work | `partial` | `:run_partial` | Returns the Run with any response produced so far |
101
+ | Cancellation stopped work | `cancelled` | `:run_cancel` | Returns the Run without a response |
102
+ | Tool input or a `ToolError` failed | The model may recover | No terminal event by itself | Gives a safe error result back to the model |
103
+ | Input, configuration, or resources failed before a Run could start | No Run exists | None | Raises the exception |
131
104
 
132
- Validation is not authorization. A tool that reads customer records, writes files, executes processes, or calls a network service must enforce the application's trust rules itself. The built-in unrestricted sandbox executes with the Ruby process's permissions and is not a security boundary. Configure an isolated sandbox before exposing filesystem or shell tools to untrusted work.
105
+ Unexpected Tool exception messages are hidden from the model. The original exception remains available to trusted application callbacks and diagnostics.
133
106
 
134
- ## Assemblies let coordination look like one agent
107
+ Failures while closing resources, delivering events, or reporting instrumentation sit outside the normal result path. They raise a Ruby exception because LittleGhost can no longer promise that it delivered a clean ending. [Running in Production](production.md) covers that boundary where applications supervise and shut down work.
135
108
 
136
- An **assembly** is any LittleGhost entrypoint that a caller can use like one agent. `CustomerSupportAgent` is therefore the smallest assembly: it contains one agent and one model loop.
109
+ ## An Assembly can look like one Agent
137
110
 
138
- When a feature needs several agents, three coordination classes preserve that same caller interface:
111
+ One model loop is not always enough. LittleGhost calls any unit that a caller can invoke like an Agent an **Assembly**.
139
112
 
140
- - A `Workflow` uses Ruby code to enforce ordering, branching, and parallel work.
141
- - A `Swarm` lets configured agents choose direct handoffs to one another.
142
- - A `Graph` follows named nodes and application-declared edges.
113
+ An Agent is the smallest Assembly. Workflow, Swarm, and Graph coordinate several participants while preserving the same entrypoints:
143
114
 
144
115
  ```ruby
145
- CustomerSupportAgent.ask("Can I get a refund?")
146
- ResponseWorkflow.ask("Can I get a refund?")
147
- ProblemSolverSwarm.ask("Can I get a refund?")
148
- SupportFlowGraph.ask("Can I get a refund?")
116
+ CustomerSupportAgent.ask(question)
117
+ ResponseWorkflow.ask(question)
118
+ ProblemSolverSwarm.ask(question)
119
+ SupportFlowGraph.ask(question)
149
120
  ```
150
121
 
151
- Each call returns a top-level `LittleGhost::Run`, and each `stream_ask` yields the same event vocabulary. The caller chooses an entrypoint without needing to branch on its internal coordination style. Instances also share `call`, `stream`, `start_execution`, interruption, and `as_tool` behavior.
122
+ That shared calling style is what makes composition feel natural. A controller, job, or CLI does not need to know whether one Agent answered or a whole support process worked together.
152
123
 
153
- Keep agent definitions in `app/agents`. Put workflow, swarm, and graph definitions in `app/assemblies`, with class names ending in `Workflow`, `Swarm`, or `Graph`. The next sections explain when each form earns its name.
124
+ ## Choose who controls the next step
154
125
 
155
- ## Subagents are model-directed delegation
126
+ The coordination types differ mainly in who decides what happens next:
156
127
 
157
- Declaring `ResearchAgent` as a subagent gives `CustomerSupportAgent` a configured set of tools for spawning, messaging, interrupting, waiting for, and listing research work:
128
+ | Need | Choose | Who controls the next step? |
129
+ | --- | --- | --- |
130
+ | One model-driven behavior | Agent | The active model loop |
131
+ | A model should delegate a named task | Subagent | The parent model |
132
+ | Ruby should enforce ordering or branching | Workflow | The workflow's Ruby code |
133
+ | Specialists should choose permitted handoffs | Swarm | The active agent |
134
+ | Allowed routes should be visible in advance | Graph | Declared nodes and edges |
158
135
 
159
- ```ruby
160
- class ResearchAgent < LittleGhost::Agent
161
- description "Investigates support questions that need broader research."
162
- model "customer_support.research"
163
- system_prompt "Return a concise evidence summary."
164
- end
136
+ ### Subagents bring in a specialist
137
+
138
+ A **subagent** is a specialist that a parent Agent can call for help. The parent model chooses when to delegate, reads the result, and then continues its own answer.
165
139
 
140
+ ```ruby
166
141
  class CustomerSupportAgent < LittleGhost::Agent
167
- model "customer_support"
168
- tools HelpCenterLookupTool
142
+ model "openrouter:openai/gpt-5.6-luna"
169
143
  subagent ResearchAgent, kind: "research"
170
144
  end
171
145
  ```
172
146
 
173
- The model decides whether to delegate and how to use the returned research. Each child declares its own tools, so access remains visible at the class receiving it. Subagent work can run concurrently and respects the configured turn, concurrency, depth, and time limits. Conversations can persist when a session store exists; `persist: false` keeps a declaration invocation-local.
147
+ Use a subagent when delegation is part of one model's decision-making. Use a Workflow when application code must guarantee that a step happens.
174
148
 
175
- Use an agent as an ordinary tool with `agent_as_tool` when one request and one result is enough. Use a subagent when the parent needs an addressable worker with follow-ups, progress, interruption, or durable conversation identity.
149
+ ### Workflows make Ruby the coordinator
176
150
 
177
- ## Workflows are application-directed composition
151
+ A **Workflow** coordinates work with ordinary Ruby. Its `perform` method can call an Agent or another Assembly, read a result, choose a branch, or run independent steps together.
178
152
 
179
- Some customer support requests must always be researched before a response is written. Put that invariant in Ruby rather than asking the model to remember it:
153
+ `invoke` prepares a lazy child call. Reading `.output` runs an intermediate child. Return the final `invoke` itself, without reading its output, so that answer can stream to the caller.
180
154
 
181
155
  ```ruby
182
156
  class ResponseWorkflow < LittleGhost::Workflow
@@ -184,9 +158,7 @@ class ResponseWorkflow < LittleGhost::Workflow
184
158
 
185
159
  def perform
186
160
  research = invoke(ResearchAgent).output
187
-
188
161
  invoke CustomerSupportAgent, input: <<~PROMPT
189
- Customer request:
190
162
  #{input.text}
191
163
 
192
164
  Research:
@@ -196,157 +168,80 @@ class ResponseWorkflow < LittleGhost::Workflow
196
168
  end
197
169
  ```
198
170
 
199
- `#invoke` builds a lazy Assembly invocation, so a workflow step may be an agent, workflow, swarm, or graph. Calling `#output` consumes an intermediate invocation; `#perform` must return its final invocation unconsumed so LittleGhost can stream it to the original caller. Input, history, state, settings, cancellation, deadline, template values, and trace parentage flow through the workflow, while intermediate usage is added to the terminal result.
171
+ Workflow children receive the caller's history and application context by default. Pass `history: []`, `context: {}`, or redacted values when a participant should receive less.
200
172
 
201
- Unlike Graph nodes and Swarm members, each Workflow invocation receives the full caller history and application context by default. Isolated copies prevent one child from mutating a sibling's context; they do not prevent disclosure. Pass `history: []`, `context: {}`, or explicitly redacted values to `invoke` when participants use different providers or privileges.
173
+ ### Swarms let agents hand work to one another
202
174
 
203
- Independent invocations can run concurrently while ordinary Ruby still controls composition:
204
-
205
- ```ruby
206
- research, verification = parallel(
207
- invoke(ResearchGraph, as: :research),
208
- invoke(VerificationWorkflow, as: :verification),
209
- max_concurrency: 2
210
- )
211
- ```
212
-
213
- Results preserve declaration order. Each branch receives isolated application context and cooperative cancellation. `timeout:`, `retries:`, `retry_on:`, and `retry_delay:` apply to `invoke`; retries require explicit exception classes because rerunning an Assembly may repeat tool side effects.
214
-
215
- Cancellation, deadlines, and step timeouts are cooperative. They do not forcibly stop provider or tool code, and they do not roll back external side effects. A participant must honor its cancellation token or deadline, and applications must decide whether an operation is safe to retry.
216
-
217
- A workflow has the same entrypoint API as an agent:
218
-
219
- ```ruby
220
- run = ResponseWorkflow.ask("Review this unusual refund request")
221
-
222
- puts run.response
223
- ```
224
-
225
- Choose a subagent when delegation is part of the model's judgment. Choose a workflow when ordering and branching are application invariants. They can coexist: `ResponseWorkflow` can always collect baseline research, while `CustomerSupportAgent` can still delegate a new question that arises while drafting the response.
226
-
227
- ## Swarms use direct agent handoffs
228
-
229
- A swarm keeps one member active at a time and injects one reserved `handoff_to_agent` tool. A member either answers the caller or hands the request directly to another configured member:
175
+ A **Swarm** is a group of Agents that can hand work to one another. One member is active at a time. It can answer the caller or choose one of its allowed specialists.
230
176
 
231
177
  ```ruby
232
178
  class ProblemSolverSwarm < LittleGhost::Swarm
233
179
  member TriageAgent
234
180
  member BillingAgent
235
181
  member AccountAgent
182
+
236
183
  start TriageAgent
237
184
  handoff TriageAgent, to: [BillingAgent, AccountAgent]
238
- max_steps 12
239
- max_handoff_repeats 3
240
185
  end
241
186
  ```
242
187
 
243
- Members are fresh Agent instances; unlike Workflow invocations and Graph nodes, Swarm members intentionally remain Agent-only so handoffs stay direct and local. A complete Swarm can still be used as a Workflow step, Graph node, or tool. A handoff names the next member and supplies a message plus optional JSON-like context. That context remains untrusted model-authored prompt content; it does not become trusted application state. A member cannot hand off to itself, hand off outside the allowed topology, or combine a handoff with another tool call. Without `handoff` declarations, routing remains all-to-all except self-handoffs. A member with no declared outgoing target receives no handoff tool. Invalid calls return an ordinary tool error so the model can recover. If no handoff occurs, the current member's response is final. Members receive only the current request or explicit handoff envelope by default; opt into original caller data with `history: true` or `context: true` on that member.
188
+ A Swarm is intentionally agent-to-agent. Its members are Agents, not other kinds of Assembly. Caller history and application context stay hidden unless a member opts in. Treat every handoff message as untrusted model input.
244
189
 
245
- Potentially intermediate model text is omitted from the caller's ordinary response stream. Streams expose Assembly lifecycle events, then the final member's ordinary response events. `max_steps` bounds total work and `max_handoff_repeats` detects repeated directed transitions; either limit raises `AssemblyLimitError` when exhausted. Members also accept the shared cooperative retry and timeout options described for workflows.
190
+ ### Graphs make routes visible
246
191
 
247
- ## Graphs guide serial and parallel paths
248
-
249
- A graph names Assembly nodes and directed edges. Ordinary edges select exactly one next node, while explicit forks and joins add bounded parallel work without shared mutable reducers:
192
+ A **Graph** connects named Assembly nodes with declared edges. Nodes can contain Agents, Workflows, Swarms, or other Graphs.
250
193
 
251
194
  ```ruby
252
195
  class SupportFlowGraph < LittleGhost::Graph
253
196
  node :triage, TriageAgent
254
- node :research, ResearchAgent
255
- node :verify, VerificationWorkflow
197
+ node :billing, BillingAgent
198
+ node :general, CustomerSupportAgent
256
199
  node :respond, CustomerSupportAgent
257
200
 
258
201
  start :triage
259
- fork :triage, to: [:research, :verify], max_concurrency: 2
260
- join [:research, :verify], to: :respond
202
+ edge :triage, :billing do |state|
203
+ state.result(:triage).output == "billing"
204
+ end
205
+ edge :triage, :general
206
+ edge :billing, :respond
207
+ edge :general, :respond
261
208
  finish :respond
262
- max_steps 12
263
209
  end
264
210
  ```
265
211
 
266
- An edge condition receives immutable `Graph::State`, including the original input, history, context, step, current and previous node names, predecessors, branch results, completed results, and a routed error when present. Exactly one matching conditional edge wins; otherwise one unconditional fallback is used. An `error_edge` can route selected application errors after retries are exhausted. Cancellation, parent deadlines, and cleanup failures always remain control flow.
267
-
268
- By default, a downstream node receives the original multimodal input plus labeled predecessor output. A join receives every branch output in declaration order. Pass `input: ->(state) { ... }` on an edge, error edge, or join to replace that mapping. Nodes receive no caller history or application context unless their declaration opts in with `history: true` or `context: true`. The original request and routed outputs still cross node and provider boundaries by default, and routing callbacks can inspect the original context through `Graph::State`; map or redact inputs explicitly when participants have different privileges. Nodes may name any Assembly type, class, builder, or immutable definition. Fork branches may follow ordinary edges before reaching their distinct declared join sources.
212
+ Graph nodes do not receive caller history or application context unless they opt in. They still receive the original request and the outputs routed to them. Use edge input mappers to choose or redact what moves forward.
269
213
 
270
- `validate!` catches invalid topology before model work begins, and `to_mermaid` renders a deterministic diagram. A graph suppresses intermediate ordinary model stream events, publishes lifecycle, transition, fork, join, retry, and error events, aggregates usage, and forwards only the finish node's ordinary response stream. Downstream nodes still receive routed outputs, and terminal step records retain bounded semantic outputs for callers to inspect.
214
+ ## Class definitions first, builders when needed
271
215
 
272
- Composite `RunResult` objects expose immutable `steps` and a `trajectory`. Step records include participants, attempts, timing, usage, relationships, and bounded semantic outputs without retaining transcripts or tool payloads. Swarm handoffs retain only their explicit handoff envelope. These records support assertions such as `result.trajectory.concurrent?(first_id, second_id)` without coupling tests to a tracing backend.
216
+ Named classes are the default way to organize reusable behavior. They are readable, load through normal Ruby conventions, and give the coordination style a visible name such as `ResponseWorkflow` or `SupportFlowGraph`.
273
217
 
274
- ## Builders unlock definitions discovered at runtime
275
-
276
- Class definitions are the default because they keep behavior, names, and topology close together. Each agent or assembly class can produce an immutable `.definition` snapshot or an independent mutable `.to_builder` variant.
277
-
278
- Use a builder when application code discovers participants or routes at runtime. The builder records the same declaration that the class DSL would organize:
218
+ Every assembly class can also produce a mutable builder:
279
219
 
280
220
  ```ruby
281
- graph = LittleGhost::GraphBuilder.new(id: "support_flow")
282
- graph.node :triage, TriageAgent
283
- graph.node :respond, CustomerSupportAgent
284
- graph.start :triage
285
- graph.edge :triage, :respond
286
- graph.finish :respond
221
+ graph = SupportFlowGraph.to_builder
222
+ graph.node :audit, AuditAgent
223
+ graph.edge :respond, :audit
224
+ graph.finish :audit
287
225
  graph.validate!
288
-
289
- run = graph.ask("Can I get a refund?")
226
+ run = graph.ask("Review order 481")
290
227
  ```
291
228
 
292
- `AgentBuilder`, `WorkflowBuilder`, `SwarmBuilder`, and `GraphBuilder` share the Assembly execution API. Builders remain mutable; each build or invocation recursively snapshots declaration containers and referenced Assembly definitions, so later builder declarations affect only future executions. Definitions are Ruby objects rather than portable JSON because conditions, callbacks, workflow bodies, factories, and resolvers may contain executable Ruby. Those closures and their external dependencies remain live trusted application code; the snapshot does not freeze state they capture.
229
+ Use a builder when trusted application configuration decides the participants or routes. Each run gets a fixed copy of the builder as it looked when the run began, so later edits affect later runs. Ruby callbacks still see any application objects they captured.
293
230
 
294
- ## Assemblies can be tools
231
+ ## One result, including the journey
295
232
 
296
- Any assembly instance supports `as_tool`. Agent classes can also declare another assembly as a tool:
233
+ Every assembly produces the same top-level `Run` and final `RunResult`. Composite assemblies also keep a size-limited record of the participants that ran:
297
234
 
298
235
  ```ruby
299
- class CustomerSupportAgent < LittleGhost::Agent
300
- assembly_as_tool SupportFlowGraph, name: "investigate_support_case"
301
- end
302
- ```
303
-
304
- `assemblies_as_tools` declares several with shared options. Existing `agent_as_tool` and `agents_as_tools` remain agent-specific aliases. Composite assemblies do not accept agent-only model or tool overrides.
305
-
306
- An assembly tool receives the invoking tool context's application state on every call. `preserve_context: false` prevents conversational history from carrying between calls; it does not suppress that application state. Nested tools must continue to authorize privileged work from trusted context rather than from model-authored input.
307
-
308
- ## Structured results separate data from prose
236
+ run = SupportFlowGraph.ask("Why was I charged twice?")
309
237
 
310
- An agent that feeds application code can declare a strict JSON object schema:
311
-
312
- ```ruby
313
- class ResearchAgent < LittleGhost::Agent
314
- model "customer_support.research"
315
- result_schema(
316
- {
317
- type: "object",
318
- properties: {
319
- summary: {type: "string"},
320
- sources: {type: "array", items: {type: "string"}}
321
- },
322
- required: %w[summary sources],
323
- additionalProperties: false
324
- },
325
- name: "support_research"
326
- )
327
- end
238
+ run.response
239
+ run.result.steps
240
+ run.result.trajectory.transitions
328
241
  ```
329
242
 
330
- LittleGhost selects provider-native structured output when the resolved model advertises it, or a strict terminal tool when supported. The locally validated value is available through `RunResult#structured_result` and `RunResult#output`. Invalid output receives one repair attempt, then raises `LittleGhost::StructuredResultError`.
331
-
332
- Use structured results when code consumes fields. Keep ordinary text when a human is the final consumer.
333
-
334
- ## Sessions preserve conversation, streams expose progress
335
-
336
- The default session store is in-memory. A configured `SessionStore` can load history and state before an agent runs and checkpoint coherent turns as work progresses. The application must supply stable session and actor identifiers when it wants continuity and isolation.
337
-
338
- Applications that need to reconcile persisted messages with invocation history can register a `LittleGhost::Runtime::Hook` and implement `session_history`. The hook receives the run plus `stored:` and `fallback:` message collections. Return the history to use, or `nil` to defer to the next hook and ultimately the session default. This keeps application-specific reconciliation policy outside the framework session type.
339
-
340
- Streams expose generic framework events rather than provider wire formats. Consumers can render text deltas, observe tool or subagent activity, collect usage, and react to terminal outcomes without coupling to OpenAI, OpenRouter, or Bedrock. The optional AG-UI adapter translates the same events at an interface boundary.
341
-
342
- ## Keep the boundary visible
343
-
344
- The core design can be summarized as five choices:
243
+ This shows callers which participants ran without including raw provider responses. A Swarm or Graph may hide intermediate model events from the public stream so the response stays coherent. That is a presentation choice, not a privacy boundary: routed outputs and step summaries still exist.
345
244
 
346
- - Put shared construction and provider policy in configuration; use inline declarations or independent YAML files according to the application's needs.
347
- - Put model behavior and available capabilities on agent classes.
348
- - Put privileged application operations behind narrow, authorized tools.
349
- - Put imperative ordering in workflows, dynamic peer routing in swarms, and guided routing in graphs.
350
- - Leave addressable background delegation to subagents.
245
+ The pieces now fit together: Agents define behavior. Tools connect them to Ruby. Runs record one execution. Assemblies let the system grow without changing the caller.
351
246
 
352
- Return to [Getting Started](getting_started.md) for the complete first-run setup. The API reference covers exact signatures and lifecycle details for `LittleGhost::Runtime`, `LittleGhost::Run`, `LittleGhost::Execution`, `LittleGhost::Assembly`, `LittleGhost::Agent`, `LittleGhost::Tool`, `LittleGhost::Workflow`, `LittleGhost::Swarm`, `LittleGhost::Graph`, and `LittleGhost::ModelResolver`.
247
+ Continue with [Compose Agents](assemblies.md) to put several agents to work together. If you are ready to connect the feature to a real application, jump to [Running in Production](production.md).