little_ghost 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +90 -84
- data/docs/guides/assemblies.md +412 -0
- data/docs/guides/code_mode.md +275 -0
- data/docs/guides/core_concepts.md +150 -239
- data/docs/guides/getting_started.md +125 -87
- data/docs/guides/integrations.md +217 -0
- data/docs/guides/models_and_providers.md +125 -0
- data/docs/guides/production.md +253 -0
- data/docs/guides/prompt_views.md +139 -0
- data/docs/guides/sandboxing.md +282 -0
- data/docs/guides/skills.md +141 -0
- data/docs/guides/structured_outputs_and_content.md +135 -0
- data/docs/guides/tools.md +329 -0
- data/lib/little_ghost/ag_ui/adapter.rb +5 -5
- data/lib/little_ghost/agent/delegation.rb +3 -3
- data/lib/little_ghost/agent/skills.rb +6 -1
- data/lib/little_ghost/agent/tool_loop.rb +6 -2
- data/lib/little_ghost/agent.rb +411 -214
- data/lib/little_ghost/agent_builder.rb +61 -22
- data/lib/little_ghost/{agent_interruptions.rb → agent_interjections.rb} +12 -12
- data/lib/little_ghost/agent_stream_source.rb +262 -0
- data/lib/little_ghost/artifact.rb +182 -0
- data/lib/little_ghost/artifacts/presentation_budget.rb +44 -0
- data/lib/little_ghost/artifacts/workspace_store.rb +370 -0
- data/lib/little_ghost/artifacts.rb +11 -0
- data/lib/little_ghost/assembly.rb +110 -30
- data/lib/little_ghost/assembly_builder.rb +59 -21
- data/lib/little_ghost/assembly_execution.rb +110 -6
- data/lib/little_ghost/code_mode/broker.rb +164 -0
- data/lib/little_ghost/code_mode/catalog.rb +54 -0
- data/lib/little_ghost/code_mode/engine.rb +58 -0
- data/lib/little_ghost/code_mode/javascript/catalog.rb +118 -0
- data/lib/little_ghost/code_mode/javascript/client.rb +550 -0
- data/lib/little_ghost/code_mode/javascript/host.rb +347 -0
- data/lib/little_ghost/code_mode/javascript/session.rb +579 -0
- data/lib/little_ghost/code_mode/javascript_engine.rb +172 -0
- data/lib/little_ghost/code_mode/protocol.rb +78 -0
- data/lib/little_ghost/code_mode/ruby/catalog.rb +80 -0
- data/lib/little_ghost/code_mode/ruby/host.rb +102 -0
- data/lib/little_ghost/code_mode/ruby/session.rb +508 -0
- data/lib/little_ghost/code_mode/ruby_engine.rb +86 -0
- data/lib/little_ghost/code_mode/runtime.rb +335 -0
- data/lib/little_ghost/code_mode/session.rb +61 -0
- data/lib/little_ghost/code_mode/types.rb +58 -0
- data/lib/little_ghost/code_mode.rb +53 -0
- data/lib/little_ghost/configuration.rb +366 -47
- data/lib/little_ghost/content.rb +24 -13
- data/lib/little_ghost/data_map.rb +209 -0
- data/lib/little_ghost/errors.rb +28 -9
- data/lib/little_ghost/execution.rb +33 -34
- data/lib/little_ghost/graph.rb +376 -197
- data/lib/little_ghost/mcp/client.rb +487 -88
- data/lib/little_ghost/mcp/toolset.rb +210 -0
- data/lib/little_ghost/mcp/types.rb +216 -0
- data/lib/little_ghost/mcp.rb +3 -0
- data/lib/little_ghost/message.rb +4 -4
- data/lib/little_ghost/model_capabilities.rb +9 -6
- data/lib/little_ghost/model_request.rb +0 -12
- data/lib/little_ghost/model_resolver.rb +15 -5
- data/lib/little_ghost/model_response.rb +3 -7
- data/lib/little_ghost/network/authorizer_server.rb +162 -0
- data/lib/little_ghost/network/certificate_authority.rb +98 -0
- data/lib/little_ghost/network/envoy_config.rb +362 -0
- data/lib/little_ghost/network/envoy_gateway.rb +409 -0
- data/lib/little_ghost/network/external_gateway.rb +68 -0
- data/lib/little_ghost/network.rb +96 -0
- data/lib/little_ghost/prompt_resolver.rb +9 -7
- data/lib/little_ghost/provider_registry.rb +3 -3
- data/lib/little_ghost/providers/anthropic.rb +8 -1
- data/lib/little_ghost/providers/gemini.rb +10 -1
- data/lib/little_ghost/providers/vertex_ai.rb +6 -1
- data/lib/little_ghost/run.rb +162 -54
- data/lib/little_ghost/run_context.rb +42 -20
- data/lib/little_ghost/run_result.rb +0 -7
- data/lib/little_ghost/runtime/hook.rb +8 -3
- data/lib/little_ghost/runtime/hooks/artifacts.rb +338 -0
- data/lib/little_ghost/runtime.rb +160 -52
- data/lib/little_ghost/sandbox/capabilities.rb +68 -0
- data/lib/little_ghost/sandbox/environment_policy.rb +41 -0
- data/lib/little_ghost/sandbox/filesystem.rb +377 -0
- data/lib/little_ghost/sandbox/isolated_backend.rb +163 -0
- data/lib/little_ghost/sandbox/limits.rb +48 -0
- data/lib/little_ghost/sandbox/mount.rb +129 -0
- data/lib/little_ghost/sandbox/network_policy.rb +108 -0
- data/lib/little_ghost/sandbox/policy.rb +88 -0
- data/lib/little_ghost/sandbox/process_runner.rb +103 -0
- data/lib/little_ghost/sandbox/process_session.rb +335 -0
- data/lib/little_ghost/sandbox/scope.rb +304 -0
- data/lib/little_ghost/sandbox.rb +158 -32
- data/lib/little_ghost/sandboxes/bubblewrap.rb +351 -0
- data/lib/little_ghost/sandboxes/native.rb +51 -0
- data/lib/little_ghost/sandboxes/seatbelt.rb +261 -0
- data/lib/little_ghost/sandboxes/unrestricted.rb +241 -0
- data/lib/little_ghost/session.rb +39 -26
- data/lib/little_ghost/session_store.rb +9 -5
- data/lib/little_ghost/session_stores/agent_core_memory.rb +64 -56
- data/lib/little_ghost/session_stores/filesystem.rb +261 -0
- data/lib/little_ghost/session_stores/memory.rb +7 -0
- data/lib/little_ghost/skills/catalog.rb +94 -14
- data/lib/little_ghost/skills/resource_root.rb +42 -0
- data/lib/little_ghost/skills/skill.rb +0 -3
- data/lib/little_ghost/stream_event.rb +8 -13
- data/lib/little_ghost/subagents/control_tool.rb +8 -0
- data/lib/little_ghost/subagents/manager.rb +53 -50
- data/lib/little_ghost/support/callbacks.rb +3 -1
- data/lib/little_ghost/support/content_capture.rb +3 -3
- data/lib/little_ghost/support/http_client.rb +2 -2
- data/lib/little_ghost/support/redactor.rb +1 -1
- data/lib/little_ghost/swarm.rb +13 -5
- data/lib/little_ghost/tool.rb +157 -64
- data/lib/little_ghost/tool_registry.rb +1 -1
- data/lib/little_ghost/tools/filesystem.rb +1 -1
- data/lib/little_ghost/tools/shell.rb +4 -3
- data/lib/little_ghost/tools/write_todos.rb +6 -1
- data/lib/little_ghost/tracing/open_telemetry.rb +1 -1
- data/lib/little_ghost/version.rb +1 -1
- data/lib/little_ghost/workflow.rb +30 -21
- data/lib/little_ghost/workspace.rb +222 -8
- data/lib/little_ghost.rb +40 -27
- metadata +104 -3
- data/lib/little_ghost/unrestricted_sandbox.rb +0 -306
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c6ec454a6085af7b0e2519eb0137e2f70115f0c00197d9f1914ac336c9fe4810
|
|
4
|
+
data.tar.gz: bde8feed25fc76a6e0290ec8462b12eb5b2df90c368214850caaa705a3da75d2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0c7081dc35068d5e3e3e0d917e4f024657cbecc117a0952b4ccfa329a9a4be2de77a96c87d9d7cf1eb3f9092219ed108d07422fa501e427542fdfb400aaf3e4a
|
|
7
|
+
data.tar.gz: 939d5fac29d8a1607225185f09784e2fe014d7ef20c20ba856ebda0b30c8e6634a79a9ff94725287f924576caecb2080204dec5fa7b36af3b801d02c3d9de93c
|
data/README.md
CHANGED
|
@@ -1,18 +1,54 @@
|
|
|
1
|
-
# Build AI features
|
|
1
|
+
# Build AI features that feel at home in Ruby
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Using a coding agent?** Start with
|
|
4
|
+
> [`llms.txt`](https://mattyr.github.io/little_ghost/llms.txt) for a concise map
|
|
5
|
+
> of the guides and API. [`llms-full.txt`](https://mattyr.github.io/little_ghost/llms-full.txt)
|
|
6
|
+
> contains the complete documentation in one file.
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
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:
|
|
6
9
|
|
|
7
|
-
|
|
10
|
+
```ruby
|
|
11
|
+
require "little_ghost"
|
|
12
|
+
|
|
13
|
+
class CustomerSupportAgent < LittleGhost::Agent
|
|
14
|
+
model "openrouter:openai/gpt-5.6-luna"
|
|
15
|
+
system_prompt "Answer customer questions clearly and concisely."
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
run = CustomerSupportAgent.ask("Draft a friendly greeting for a customer.")
|
|
19
|
+
run.response
|
|
20
|
+
# One possible response: Hi! How can I help today?
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
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.
|
|
24
|
+
|
|
25
|
+
Model requests may send system instructions, caller input, conversation history,
|
|
26
|
+
Tool results, and attachments to the selected provider. Model wording can vary
|
|
27
|
+
between runs. [Models and Providers](docs/guides/models_and_providers.md) explains
|
|
28
|
+
how to choose where each Agent sends its requests.
|
|
8
29
|
|
|
9
|
-
|
|
30
|
+
## Install the gem
|
|
10
31
|
|
|
11
|
-
|
|
32
|
+
LittleGhost requires Ruby 3.3 or newer. Add it to your bundle and provide a provider credential:
|
|
12
33
|
|
|
13
34
|
```ruby
|
|
14
|
-
|
|
35
|
+
gem "little_ghost"
|
|
36
|
+
```
|
|
15
37
|
|
|
38
|
+
```sh
|
|
39
|
+
$ bundle install
|
|
40
|
+
$ export OPENROUTER_API_KEY="..."
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
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.
|
|
44
|
+
|
|
45
|
+
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`.
|
|
46
|
+
|
|
47
|
+
## Give an agent real capabilities
|
|
48
|
+
|
|
49
|
+
Tools let an agent call focused parts of your application:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
16
52
|
class HelpCenterLookupTool < LittleGhost::Tool
|
|
17
53
|
description "Look up a help center entry by topic."
|
|
18
54
|
input_schema(
|
|
@@ -29,39 +65,39 @@ class HelpCenterLookupTool < LittleGhost::Tool
|
|
|
29
65
|
end
|
|
30
66
|
|
|
31
67
|
class CustomerSupportAgent < LittleGhost::Agent
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
system_prompt "Answer clearly. Check the help center before stating company guidance."
|
|
68
|
+
model "openrouter:openai/gpt-5.6-luna"
|
|
69
|
+
system_prompt "Check the help center before stating company guidance."
|
|
35
70
|
tools HelpCenterLookupTool
|
|
36
71
|
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
72
|
```
|
|
43
73
|
|
|
44
|
-
The
|
|
74
|
+
The schema checks the shape of the input. Your Ruby code still decides whether
|
|
75
|
+
the operation is allowed. The result goes back to the model as context.
|
|
45
76
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
```
|
|
77
|
+
An ordinary Tool runs in your Ruby process. When a Tool needs files or child
|
|
78
|
+
processes, it can delegate that work through a Sandbox. Code mode goes one step
|
|
79
|
+
further: a sandboxed interpreter can compose several Tools, while every Tool
|
|
80
|
+
call still returns to your Ruby Tool for validation and permission checks.
|
|
51
81
|
|
|
52
|
-
##
|
|
82
|
+
## Grow without changing the caller
|
|
53
83
|
|
|
54
|
-
|
|
84
|
+
An **agent** owns one model loop. An **assembly** is one or more agents working as a unit. You call either one the same way:
|
|
55
85
|
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
└────────> ResearchAgent subagent
|
|
86
|
+
```ruby
|
|
87
|
+
CustomerSupportAgent.ask(question)
|
|
88
|
+
ResponseWorkflow.ask(question)
|
|
89
|
+
ProblemSolverSwarm.ask(question)
|
|
90
|
+
SupportFlowGraph.ask(question)
|
|
62
91
|
```
|
|
63
92
|
|
|
64
|
-
|
|
93
|
+
Choose the coordination style that matches who should control the next step:
|
|
94
|
+
|
|
95
|
+
- A **subagent** lets a model delegate an addressable task.
|
|
96
|
+
- A **workflow** uses ordinary Ruby for ordering and branching.
|
|
97
|
+
- A **swarm** lets configured agents choose permitted handoffs.
|
|
98
|
+
- A **graph** makes allowed routes explicit as nodes and edges.
|
|
99
|
+
|
|
100
|
+
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.
|
|
65
101
|
|
|
66
102
|
```text
|
|
67
103
|
request ──> CustomerSupportAgent
|
|
@@ -73,67 +109,37 @@ request ──> ProblemSolverSwarm ──> TriageAgent ──handoff──> Bill
|
|
|
73
109
|
request ──> SupportFlowGraph ──> TriageAgent ──edge──> ResponseAgent
|
|
74
110
|
```
|
|
75
111
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
Each top-level execution owns one run lifecycle. The run checkpoints session state, closes its resources, aggregates usage, and emits framework events regardless of which assembly type is the entrypoint.
|
|
81
|
-
|
|
82
|
-
## Installation and configuration
|
|
83
|
-
|
|
84
|
-
LittleGhost requires Ruby 3.3 or newer. Add it to your bundle:
|
|
85
|
-
|
|
86
|
-
```ruby
|
|
87
|
-
gem "little_ghost"
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
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.
|
|
91
|
-
|
|
92
|
-
Configuration does not require a particular directory layout. Provider connections and model profiles resolve independently in this order:
|
|
93
|
-
|
|
94
|
-
1. An inline `config.providers` or `config.models` declaration.
|
|
95
|
-
2. The corresponding explicit `config.providers_path` or `config.models_path`.
|
|
96
|
-
3. The optional conventional file under `config/little_ghost/`.
|
|
97
|
-
4. Environment-based provider selection and the built-in `default` profile.
|
|
98
|
-
|
|
99
|
-
An explicit path must exist. A missing conventional file is valid. The conventional form keeps connection policy separate from model roles:
|
|
100
|
-
|
|
101
|
-
```yaml
|
|
102
|
-
# config/little_ghost/providers.yml
|
|
103
|
-
providers:
|
|
104
|
-
openai:
|
|
105
|
-
adapter: openai
|
|
106
|
-
api_key: <%= ENV.fetch("OPENAI_API_KEY") %>
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
```yaml
|
|
110
|
-
# config/little_ghost/models.yml
|
|
111
|
-
default_model: customer_support
|
|
112
|
-
models:
|
|
113
|
-
customer_support:
|
|
114
|
-
target: openai:gpt-5.6-luna
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
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.
|
|
118
|
-
|
|
119
|
-
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.
|
|
112
|
+
The result stays familiar too. Every call returns a `Run` with the response,
|
|
113
|
+
outcome, usage, and any final error. A coordinated assembly also records which
|
|
114
|
+
participants ran. Use `.stream_ask` to watch the work as it happens.
|
|
120
115
|
|
|
121
|
-
LittleGhost
|
|
116
|
+
LittleGhost is pre-1.0. Pin the gem version and review release notes before
|
|
117
|
+
upgrading, because interfaces may change between releases.
|
|
122
118
|
|
|
123
|
-
##
|
|
119
|
+
## Keep going
|
|
124
120
|
|
|
125
|
-
- [Getting Started](docs/guides/getting_started.md)
|
|
126
|
-
- [Core Concepts](docs/guides/core_concepts.md)
|
|
127
|
-
- [
|
|
121
|
+
- [Getting Started](docs/guides/getting_started.md) takes you from installation to a tool-backed, streaming agent.
|
|
122
|
+
- [Core Concepts](docs/guides/core_concepts.md) builds the mental model from Agent to Assembly.
|
|
123
|
+
- [Models and Providers](docs/guides/models_and_providers.md) gives shared model choices application-facing names.
|
|
124
|
+
- [Prompts as Views](docs/guides/prompt_views.md) gives growing instructions, shared pieces, and application values a natural home.
|
|
125
|
+
- [Tools](docs/guides/tools.md) explains how models call focused Ruby operations.
|
|
126
|
+
- [Structured Results and Content](docs/guides/structured_outputs_and_content.md) covers checked result shapes, images, and documents.
|
|
127
|
+
- [Compose Agents](docs/guides/assemblies.md) walks through workflows, swarms, graphs, nesting, and builders.
|
|
128
|
+
- [Skills](docs/guides/skills.md) organizes reusable instructions and supporting resources.
|
|
129
|
+
- [Workspaces and Sandboxes](docs/guides/sandboxing.md) gives files and child processes a deliberate place to run.
|
|
130
|
+
- [Code Mode](docs/guides/code_mode.md) lets a model compose Tools in sandboxed Ruby or optional JavaScript.
|
|
131
|
+
- [Integrations](docs/guides/integrations.md) connects MCP, AG-UI, and OpenTelemetry.
|
|
132
|
+
- [Running in Production](docs/guides/production.md) covers configuration, saved conversations, supervision, and observability.
|
|
133
|
+
- [API reference](rdoc-ref:LittleGhost) provides exact method signatures and ownership rules.
|
|
128
134
|
|
|
129
|
-
|
|
135
|
+
### For contributors
|
|
130
136
|
|
|
131
137
|
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).
|
|
132
138
|
|
|
133
139
|
```sh
|
|
134
|
-
bundle install
|
|
135
|
-
bundle exec rake test
|
|
136
|
-
bundle exec standardrb --no-fix
|
|
140
|
+
$ bundle install
|
|
141
|
+
$ bundle exec rake test
|
|
142
|
+
$ bundle exec standardrb --no-fix
|
|
137
143
|
```
|
|
138
144
|
|
|
139
|
-
LittleGhost is
|
|
145
|
+
LittleGhost is available under the MIT License.
|
|
@@ -0,0 +1,412 @@
|
|
|
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
|
+
## Call every assembly the same way
|
|
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
|
|
140
|
+
coherent public answer. The next member still receives the handoff, and the
|
|
141
|
+
result keeps a bounded summary of the journey.
|
|
142
|
+
|
|
143
|
+
## Use a Graph for guided routes
|
|
144
|
+
|
|
145
|
+
A Graph names the possible stops and the routes between them. Start with a conditional route before adding parallel branches:
|
|
146
|
+
|
|
147
|
+
```ruby
|
|
148
|
+
class SupportFlowGraph < LittleGhost::Graph
|
|
149
|
+
node :triage, TriageAgent
|
|
150
|
+
node :billing, BillingAgent
|
|
151
|
+
node :general, CustomerSupportAgent
|
|
152
|
+
node :respond, CustomerSupportAgent
|
|
153
|
+
|
|
154
|
+
start :triage
|
|
155
|
+
|
|
156
|
+
edge :triage, :billing do |state|
|
|
157
|
+
state.result(:triage).output == "billing"
|
|
158
|
+
end
|
|
159
|
+
edge :triage, :general
|
|
160
|
+
edge :billing, :respond
|
|
161
|
+
edge :general, :respond
|
|
162
|
+
finish :respond
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
SupportFlowGraph.validate!
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Conditions and input mappers read an immutable `Graph::State`. At most one conditional route may match. If several match, LittleGhost raises `AssemblyRoutingError` instead of guessing which one wins. One unconditional edge can catch the request when none match.
|
|
169
|
+
|
|
170
|
+
Graph nodes start without caller history or application context. The start node receives the original input. By default, each downstream node receives the original task plus its immediate predecessor results. Use an input mapper to replace or redact that data before it moves to a provider or participant that should see less.
|
|
171
|
+
|
|
172
|
+
Opt in when a node needs caller context:
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
node :account_lookup, AccountLookupAgent, context: true
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Run bounded parallel paths
|
|
179
|
+
|
|
180
|
+
Give one node several unconditional edges when its result should start independent branches. LittleGhost finds their first unambiguous common successor and waits for every branch before running it:
|
|
181
|
+
|
|
182
|
+
```ruby
|
|
183
|
+
class InvestigationGraph < LittleGhost::Graph
|
|
184
|
+
node :triage, TriageAgent
|
|
185
|
+
node :ledger, LedgerResearchAgent
|
|
186
|
+
node :policy, PolicyResearchAgent
|
|
187
|
+
node :respond, CustomerSupportAgent
|
|
188
|
+
|
|
189
|
+
start :triage
|
|
190
|
+
edge :triage, :ledger
|
|
191
|
+
edge :triage, :policy
|
|
192
|
+
edge :ledger, :respond
|
|
193
|
+
edge :policy, :respond
|
|
194
|
+
finish :respond
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Set `max_concurrency` on the Graph to bound every parallel group. An edge with
|
|
199
|
+
an array target can declare the group explicitly and override that bound:
|
|
200
|
+
|
|
201
|
+
```ruby
|
|
202
|
+
max_concurrency 4
|
|
203
|
+
edge :triage, [:ledger, :policy], max_concurrency: 2
|
|
204
|
+
edge [:ledger, :policy], :respond
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
#### Join parallel branches
|
|
208
|
+
|
|
209
|
+
An array source declares a wait-for-all convergence. Use it when the common
|
|
210
|
+
successor cannot be inferred or when the convergence needs its own input
|
|
211
|
+
mapper. Parallel groups cannot nest. `validate!` raises `ConfigurationError`
|
|
212
|
+
when inference has no single convergence, finds competing routes at a branch
|
|
213
|
+
boundary, or encounters overlapping or nested groups.
|
|
214
|
+
|
|
215
|
+
The first nodes in a parallel group receive the original task and the source
|
|
216
|
+
result. The convergence target receives the original task and each immediate
|
|
217
|
+
predecessor result in declaration order. LittleGhost labels them as context:
|
|
218
|
+
|
|
219
|
+
```text
|
|
220
|
+
Original Task:
|
|
221
|
+
Why is transfer 481 pending?
|
|
222
|
+
|
|
223
|
+
Inputs from previous nodes:
|
|
224
|
+
|
|
225
|
+
From ledger:
|
|
226
|
+
The ledger entry is awaiting settlement.
|
|
227
|
+
|
|
228
|
+
From policy:
|
|
229
|
+
Pending transfers usually settle within two business days.
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
#### Map inputs between nodes
|
|
233
|
+
|
|
234
|
+
An `input` mapper replaces this default with the exact value returned by the
|
|
235
|
+
mapper. Put it on an edge to control one transition, or on a node to control
|
|
236
|
+
every route into that target. A selected edge or edge-group mapper takes
|
|
237
|
+
precedence over the target node mapper:
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
edge :triage, :ledger, input: lambda { |state|
|
|
241
|
+
"Investigate this transfer:\n#{state.result(:triage).output}"
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Conditions and mappers receive a copied, frozen `Graph::State`, so they cannot
|
|
246
|
+
change the running Graph.
|
|
247
|
+
Use `state.input` for the original request, `state.results` for completed nodes,
|
|
248
|
+
and `state.incoming_results` for the immediate predecessors. The
|
|
249
|
+
`LittleGhost::Graph::State` API reference lists every routing value.
|
|
250
|
+
|
|
251
|
+
Conditions and mappers are application code. Their state includes copies of
|
|
252
|
+
caller history and application context, even when the destination node does not
|
|
253
|
+
receive those values.
|
|
254
|
+
|
|
255
|
+
Use an explicit array-source edge when a fan-in needs one mapper:
|
|
256
|
+
|
|
257
|
+
```ruby
|
|
258
|
+
edge [:ledger, :policy], :respond, input: lambda { |state|
|
|
259
|
+
JSON.generate(state.incoming_results.transform_values(&:output))
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
#### Control data crossing branches
|
|
264
|
+
|
|
265
|
+
By default, the original request and complete source output go to every
|
|
266
|
+
parallel branch. An input mapper can replace the branch input, and a redaction
|
|
267
|
+
assembly before the fan-out can narrow the source output. Use those options
|
|
268
|
+
when a participant or provider should receive only part of the data.
|
|
269
|
+
|
|
270
|
+
#### Recover and review
|
|
271
|
+
|
|
272
|
+
An error edge can send an expected failure to a recovery Assembly. Call `validate!` before the first run.
|
|
273
|
+
|
|
274
|
+
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.
|
|
275
|
+
|
|
276
|
+
## Make retries safe
|
|
277
|
+
|
|
278
|
+
Workflow calls, Swarm members, and Graph nodes can set timeouts and retries. Use them for work that can safely be attempted again:
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
invoke(
|
|
282
|
+
ResearchAgent,
|
|
283
|
+
timeout: 15,
|
|
284
|
+
retries: 2,
|
|
285
|
+
retry_on: [LittleGhost::ProviderError],
|
|
286
|
+
retry_delay: 0.25
|
|
287
|
+
)
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
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.
|
|
291
|
+
|
|
292
|
+
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.
|
|
293
|
+
|
|
294
|
+
## Watch every agent in an assembly
|
|
295
|
+
|
|
296
|
+
Follow each participant while a composite assembly runs by handling its contextual `:agent_stream` events. These events arrive alongside the coherent public answer and assembly lifecycle events:
|
|
297
|
+
|
|
298
|
+
```ruby
|
|
299
|
+
stream = SupportFlowGraph.stream_ask("Why was I charged twice?")
|
|
300
|
+
|
|
301
|
+
run = stream.each do |event|
|
|
302
|
+
next unless event.type == :agent_stream
|
|
303
|
+
|
|
304
|
+
source = event.data.fetch(:source)
|
|
305
|
+
agent_event = event.data.fetch(:event)
|
|
306
|
+
participant = source.assembly_path.last&.participant || source.agent_id
|
|
307
|
+
|
|
308
|
+
case agent_event.type
|
|
309
|
+
when :invocation_start
|
|
310
|
+
routed_input = event.data.fetch(:input)
|
|
311
|
+
render_input(participant, routed_input)
|
|
312
|
+
when :text_delta
|
|
313
|
+
publish_progress(participant, agent_event.data.fetch(:text))
|
|
314
|
+
when :invocation_stop
|
|
315
|
+
record_result(participant, agent_event.data.fetch(:result))
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
run.completed? # => true
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
`source.agent_id` identifies the Agent class, `source.agent_path` distinguishes managed subagents, and `source.operation_id` groups one invocation. `source.assembly_path` lists the enclosing Workflow, Swarm, or Graph steps from the outside inward.
|
|
323
|
+
|
|
324
|
+
The routed input and inner event are copied and frozen before they reach the
|
|
325
|
+
observer, so changing an event can't affect the running assembly. Parallel
|
|
326
|
+
participants can interleave. Events from each Agent retain their order, and
|
|
327
|
+
LittleGhost never calls the stream block concurrently.
|
|
328
|
+
|
|
329
|
+
The contextual wrapper arrives before the corresponding ordinary event. An assembly's final Agent therefore appears through both projections. Filter for `:agent_stream` when building an all-agent view, or handle ordinary events when rendering only the final answer. Pass `include_agent_events: false` when a composite assembly caller only wants the ordinary public stream. Standalone Agent streams keep their ordinary events by default and accept `include_agent_events: true` when source metadata is useful.
|
|
330
|
+
|
|
331
|
+
The AG-UI adapter ignores contextual wrappers. Translate them explicitly if an AG-UI client should receive participant activity.
|
|
332
|
+
|
|
333
|
+
> **Safety note:** A composite stream can include inputs, reasoning, Tool
|
|
334
|
+
> arguments and results, errors, and output from every participant. Check that
|
|
335
|
+
> the destination may see the complete Run, or filter the events before sending
|
|
336
|
+
> or storing them.
|
|
337
|
+
|
|
338
|
+
## Inspect what the assembly did
|
|
339
|
+
|
|
340
|
+
A composite result remembers the steps it took. `trajectory` lets you explore them:
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
run = InvestigationGraph.ask("Why is transfer 481 pending?")
|
|
344
|
+
trajectory = run.result.trajectory
|
|
345
|
+
|
|
346
|
+
trajectory.each { |step| puts "#{step.participant}: #{step.status}" }
|
|
347
|
+
trajectory.transitions
|
|
348
|
+
ledger = trajectory.find { |step| step.participant == "ledger" }
|
|
349
|
+
policy = trajectory.find { |step| step.participant == "policy" }
|
|
350
|
+
trajectory.concurrent?(ledger.id, policy.id)
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
Step outputs and buffered events have size limits. Use your application's
|
|
354
|
+
instrumentation when you need deeper diagnostics.
|
|
355
|
+
|
|
356
|
+
## Compose assemblies inside assemblies
|
|
357
|
+
|
|
358
|
+
Workflow and Graph participants accept any Assembly definition:
|
|
359
|
+
|
|
360
|
+
```ruby
|
|
361
|
+
class ResolutionGraph < LittleGhost::Graph
|
|
362
|
+
node :investigate, InvestigationWorkflow
|
|
363
|
+
node :resolve, ProblemSolverSwarm
|
|
364
|
+
|
|
365
|
+
start :investigate
|
|
366
|
+
edge :investigate, :resolve
|
|
367
|
+
finish :resolve
|
|
368
|
+
end
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
An Assembly can also become an Agent tool:
|
|
372
|
+
|
|
373
|
+
```ruby
|
|
374
|
+
class SupportCoordinatorAgent < LittleGhost::Agent
|
|
375
|
+
assembly_as_tool InvestigationGraph,
|
|
376
|
+
name: "investigate_support_request",
|
|
377
|
+
preserve_context: false
|
|
378
|
+
end
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
The nested assembly receives the parent Tool's current working state. That state
|
|
382
|
+
may include values restored from a Session. `preserve_context` controls
|
|
383
|
+
conversation history only: when it is false, working state still passes to the
|
|
384
|
+
nested assembly. A nested Tool that reads private data or performs a write
|
|
385
|
+
should check values established for the current request or checked again after
|
|
386
|
+
loading.
|
|
387
|
+
|
|
388
|
+
## Reach for builders when definitions are dynamic
|
|
389
|
+
|
|
390
|
+
Classes are the preferred form in application code. Use a builder when runtime
|
|
391
|
+
configuration decides the nodes or routes:
|
|
392
|
+
|
|
393
|
+
```ruby
|
|
394
|
+
graph = LittleGhost::GraphBuilder.new(
|
|
395
|
+
id: "support_flow",
|
|
396
|
+
description: "Routes customer support requests"
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
graph.node :triage, TriageAgent
|
|
400
|
+
graph.node :respond, CustomerSupportAgent
|
|
401
|
+
graph.start :triage
|
|
402
|
+
graph.edge :triage, :respond
|
|
403
|
+
graph.finish :respond
|
|
404
|
+
graph.validate!
|
|
405
|
+
|
|
406
|
+
run = graph.ask("Where is my order?")
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
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.
|
|
410
|
+
|
|
411
|
+
Continue with [Skills](skills.md) when an Agent should discover focused
|
|
412
|
+
instructions and supporting resources only when a task needs them.
|