little_ghost 0.4.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 +28 -6
- data/docs/guides/assemblies.md +143 -17
- data/docs/guides/code_mode.md +275 -0
- data/docs/guides/core_concepts.md +46 -30
- data/docs/guides/getting_started.md +17 -6
- data/docs/guides/integrations.md +217 -0
- data/docs/guides/models_and_providers.md +125 -0
- data/docs/guides/production.md +81 -15
- data/docs/guides/prompt_views.md +14 -7
- 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 +2 -2
- data/lib/little_ghost/agent/delegation.rb +2 -2
- 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 +248 -46
- data/lib/little_ghost/agent_builder.rb +61 -22
- 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 +63 -17
- data/lib/little_ghost/assembly_builder.rb +26 -26
- data/lib/little_ghost/assembly_execution.rb +23 -2
- 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 +107 -12
- data/lib/little_ghost/content.rb +19 -8
- data/lib/little_ghost/data_map.rb +2 -2
- data/lib/little_ghost/errors.rb +26 -7
- data/lib/little_ghost/execution.rb +1 -2
- data/lib/little_ghost/graph.rb +367 -207
- 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/model_capabilities.rb +9 -6
- data/lib/little_ghost/model_request.rb +0 -12
- data/lib/little_ghost/model_resolver.rb +13 -3
- 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 +7 -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 +83 -13
- data/lib/little_ghost/run_context.rb +9 -0
- data/lib/little_ghost/run_result.rb +0 -7
- data/lib/little_ghost/runtime/hook.rb +5 -0
- data/lib/little_ghost/runtime/hooks/artifacts.rb +338 -0
- data/lib/little_ghost/runtime.rb +102 -34
- 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 +30 -6
- 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 +12 -9
- 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/tool.rb +104 -53
- 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/version.rb +1 -1
- data/lib/little_ghost/workspace.rb +222 -8
- data/lib/little_ghost.rb +21 -12
- metadata +98 -2
- data/lib/little_ghost/unrestricted_sandbox.rb +0 -306
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Return Structured Results and Send Rich Content
|
|
2
|
+
|
|
3
|
+
An Agent can return a checked Ruby value instead of prose. It can also receive
|
|
4
|
+
text alongside images and documents when the selected model supports them.
|
|
5
|
+
|
|
6
|
+
## Declare a result shape
|
|
7
|
+
|
|
8
|
+
Use `result_schema` on the Agent:
|
|
9
|
+
|
|
10
|
+
```ruby
|
|
11
|
+
class SupportTriageAgent < LittleGhost::Agent
|
|
12
|
+
model :customer_support
|
|
13
|
+
system_prompt "Classify the request using only the supplied evidence."
|
|
14
|
+
|
|
15
|
+
result_schema(
|
|
16
|
+
name: "support_triage",
|
|
17
|
+
description: "Routing decision for one support request",
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: {
|
|
20
|
+
category: {
|
|
21
|
+
type: "string",
|
|
22
|
+
enum: %w[billing delivery returns other]
|
|
23
|
+
},
|
|
24
|
+
urgent: {type: "boolean"},
|
|
25
|
+
summary: {type: "string", maxLength: 500}
|
|
26
|
+
},
|
|
27
|
+
required: %w[category urgent summary],
|
|
28
|
+
additionalProperties: false
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
run = SupportTriageAgent.ask("My package is missing and I leave tomorrow.")
|
|
33
|
+
|
|
34
|
+
if run.completed?
|
|
35
|
+
triage = run.result.output
|
|
36
|
+
route_request(category: triage.fetch("category"), urgent: triage.fetch("urgent"))
|
|
37
|
+
else
|
|
38
|
+
report_failure(run.error)
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Every object in a result schema must set `additionalProperties: false` and list
|
|
43
|
+
every property in `required`. The top-level type must be `object`. LittleGhost
|
|
44
|
+
checks the supported JSON Schema subset when the Agent is defined and checks
|
|
45
|
+
the returned value again before publishing a `LittleGhost::StructuredResult`.
|
|
46
|
+
|
|
47
|
+
LittleGhost supports a focused subset of [JSON Schema Draft
|
|
48
|
+
2020-12](https://json-schema.org/draft/2020-12/json-schema-core). The
|
|
49
|
+
`LittleGhost::Agent.result_schema` API reference lists the accepted keywords.
|
|
50
|
+
|
|
51
|
+
For a structured Agent, `run.result.output` returns the checked value. For an
|
|
52
|
+
ordinary Agent, it returns text. Use `run.result.structured_result` when you
|
|
53
|
+
also need the schema name.
|
|
54
|
+
|
|
55
|
+
## Let LittleGhost choose the strategy
|
|
56
|
+
|
|
57
|
+
The default `strategy: :auto` uses provider-native structured output when it is
|
|
58
|
+
available and otherwise uses a terminal Tool when the model supports reliable
|
|
59
|
+
Tool calls. You can require one strategy:
|
|
60
|
+
|
|
61
|
+
```ruby
|
|
62
|
+
result_schema(
|
|
63
|
+
{
|
|
64
|
+
type: "object",
|
|
65
|
+
properties: {answer: {type: "string"}},
|
|
66
|
+
required: ["answer"],
|
|
67
|
+
additionalProperties: false
|
|
68
|
+
},
|
|
69
|
+
name: "answer",
|
|
70
|
+
strategy: :provider
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Use `:provider` or `:tool` when your application depends on that exact path.
|
|
75
|
+
LittleGhost raises `LittleGhost::ConfigurationError` before execution if the
|
|
76
|
+
selected model cannot provide it.
|
|
77
|
+
|
|
78
|
+
If the first response is missing or invalid, LittleGhost asks the model to
|
|
79
|
+
repair it once. A second invalid response ends the Run with
|
|
80
|
+
`LittleGhost::StructuredResultError`.
|
|
81
|
+
|
|
82
|
+
> **Safety note:** A checked shape tells you that fields and types match the
|
|
83
|
+
> schema, not that the model's claims are correct. Apply your normal business
|
|
84
|
+
> checks before the result changes data, spends money, or contacts someone.
|
|
85
|
+
|
|
86
|
+
## Combine text with an image
|
|
87
|
+
|
|
88
|
+
Build a user `Message` from typed content blocks:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
image = LittleGhost::Content::Image.new(
|
|
92
|
+
data: File.binread("tmp/damaged-package.png"),
|
|
93
|
+
media_type: "image/png"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
message = LittleGhost::Message.new(
|
|
97
|
+
role: :user,
|
|
98
|
+
content: [
|
|
99
|
+
LittleGhost::Content::Text.new(
|
|
100
|
+
text: "Describe the visible damage without guessing its cause."
|
|
101
|
+
),
|
|
102
|
+
image
|
|
103
|
+
]
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
run = DamageReviewAgent.ask(message)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
For a document, include a display name:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
document = LittleGhost::Content::Document.new(
|
|
113
|
+
data: File.binread("tmp/refund-guide.pdf"),
|
|
114
|
+
media_type: "application/pdf",
|
|
115
|
+
name: "refund-guide.pdf"
|
|
116
|
+
)
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`Image` and `Document` hold the original bytes. Serialization base64-encodes
|
|
120
|
+
them when they cross a JSON boundary, which increases request size. The
|
|
121
|
+
resolved model checks the content type against its advertised input
|
|
122
|
+
capabilities before making the provider request.
|
|
123
|
+
|
|
124
|
+
> **Safety note:** When content comes from an upload, check its size and actual
|
|
125
|
+
> file type before creating the block. Send it only to a provider you use for
|
|
126
|
+
> that kind of data, and don't treat text extracted from a file as proof of
|
|
127
|
+
> identity or permission.
|
|
128
|
+
|
|
129
|
+
Content blocks may also accompany a Tool result. They become visible to the
|
|
130
|
+
model, so apply the same size and disclosure checks as you would for user
|
|
131
|
+
content.
|
|
132
|
+
|
|
133
|
+
Continue with [Compose Agents](assemblies.md) to route a checked result through
|
|
134
|
+
several participants, or [Tools](tools.md) when the result should call Ruby
|
|
135
|
+
code.
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# Give agents tools
|
|
2
|
+
|
|
3
|
+
Tools turn an agent from a writer into a participant in your application. Each
|
|
4
|
+
Tool offers one named operation with checked input. Your Ruby code decides what
|
|
5
|
+
the operation may do.
|
|
6
|
+
|
|
7
|
+
Start with a narrow read:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
class HelpCenterLookupTool < LittleGhost::Tool
|
|
11
|
+
description "Look up a help center entry by topic."
|
|
12
|
+
input_schema(
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {topic: {type: "string", enum: %w[refunds shipping]}},
|
|
15
|
+
required: ["topic"],
|
|
16
|
+
additionalProperties: false
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
def call(input)
|
|
20
|
+
{
|
|
21
|
+
"refunds" => "Refunds are available within 30 days of purchase.",
|
|
22
|
+
"shipping" => "Standard shipping takes three to five business days."
|
|
23
|
+
}.fetch(input.fetch("topic"))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class CustomerSupportAgent < LittleGhost::Agent
|
|
28
|
+
system_prompt "Check the help center before stating company guidance."
|
|
29
|
+
tools HelpCenterLookupTool
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The model sees the Tool's name, description, and input schema. When it chooses
|
|
34
|
+
the Tool, LittleGhost checks the arguments, calls `#call`, and returns the
|
|
35
|
+
result to the model. That loop can happen several times before the Agent writes
|
|
36
|
+
its final answer.
|
|
37
|
+
|
|
38
|
+
## Declare one Tool or a collection
|
|
39
|
+
|
|
40
|
+
An Agent's `tools` declaration accepts one or more classes. Pass a Tool class
|
|
41
|
+
directly for one operation:
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
class CustomerSupportAgent < LittleGhost::Agent
|
|
45
|
+
tools HelpCenterLookupTool
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Pass several classes in one declaration, or use several declarations. They are
|
|
50
|
+
equivalent and inherited declarations are retained:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
class CustomerSupportAgent < LittleGhost::Agent
|
|
54
|
+
tools HelpCenterLookupTool, OrderStatusTool
|
|
55
|
+
tools EscalateConversationTool
|
|
56
|
+
end
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
For a related or dynamically discovered collection, pass a provider class that
|
|
60
|
+
implements `self.tools(binding)`. It may return Tool classes, Tool instances,
|
|
61
|
+
nested arrays, or `nil`; LittleGhost flattens the result and binds every Tool to
|
|
62
|
+
the current run:
|
|
63
|
+
|
|
64
|
+
```ruby
|
|
65
|
+
class AccountTools
|
|
66
|
+
def self.tools(binding)
|
|
67
|
+
[
|
|
68
|
+
AccountStatusTool,
|
|
69
|
+
(CloseAccountTool if binding.run.invocation.context["may_close_account"])
|
|
70
|
+
]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class CustomerSupportAgent < LittleGhost::Agent
|
|
75
|
+
tools HelpCenterLookupTool, AccountTools
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Prefer `available_if` on an individual Tool when only that operation is
|
|
80
|
+
conditional. Use a provider when the collection itself owns discovery,
|
|
81
|
+
construction, or shared setup for an application or remote service.
|
|
82
|
+
|
|
83
|
+
LittleGhost does not enforce class names. A useful application convention is
|
|
84
|
+
to end one model-callable operation with `Tool` and a provider of multiple
|
|
85
|
+
operations with `Tools`—for example, `OrderStatusTool` and `AccountTools`.
|
|
86
|
+
The suffix makes `tools AccountTools` readable without introducing a framework
|
|
87
|
+
base class or hiding ordinary Ruby composition.
|
|
88
|
+
|
|
89
|
+
## Check permission in Ruby
|
|
90
|
+
|
|
91
|
+
A schema answers “Is this input shaped correctly?” It does not answer “May this
|
|
92
|
+
caller perform this operation?” Use values established by your application to
|
|
93
|
+
answer that second question.
|
|
94
|
+
For sensitive work, read those values inside the Tool:
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
class OrderStatusTool < LittleGhost::Tool
|
|
98
|
+
description "Look up an order for the current customer."
|
|
99
|
+
input_schema(
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {order_number: {type: "string"}},
|
|
102
|
+
required: ["order_number"],
|
|
103
|
+
additionalProperties: false
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
def call(input)
|
|
107
|
+
Orders.status_for(
|
|
108
|
+
actor_id: run.invocation.actor_id,
|
|
109
|
+
account_id: run.invocation.context.fetch("account_id"),
|
|
110
|
+
order_number: input.fetch("order_number")
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Here, the model chooses `order_number`. The application supplies `actor_id` and
|
|
117
|
+
`account_id` after authenticating the request. The Tool reads those values
|
|
118
|
+
through its run-scoped binding: the objects LittleGhost attaches to a Tool for
|
|
119
|
+
one execution. They are not part of the model's Tool arguments.
|
|
120
|
+
|
|
121
|
+
The Run also has `context.state`, mutable working state for this execution. When
|
|
122
|
+
saved conversations are configured, it may contain values from an earlier Run.
|
|
123
|
+
Recheck saved values before using them for a permission decision.
|
|
124
|
+
|
|
125
|
+
> **Safety note:** Keep identity and account membership in the Run's invocation,
|
|
126
|
+
> not in model-selected arguments. A valid Tool input may still name a record
|
|
127
|
+
> the current caller isn't allowed to use.
|
|
128
|
+
|
|
129
|
+
## Know where a Tool runs
|
|
130
|
+
|
|
131
|
+
A local Tool is application code. Its `#call` method runs in the same Ruby
|
|
132
|
+
process as LittleGhost, with the same access as the rest of your application. A
|
|
133
|
+
Sandbox contains only work that the Tool explicitly sends through it.
|
|
134
|
+
|
|
135
|
+
Some Tools deliberately delegate a smaller operation to the Sandbox:
|
|
136
|
+
|
|
137
|
+
- `LittleGhost::Tools::Filesystem` reads and changes paths through the bound
|
|
138
|
+
Sandbox. It exposes mutation Tools only when that Sandbox is writable.
|
|
139
|
+
- `LittleGhost::Tools::Shell` runs one argument vector through the bound
|
|
140
|
+
Sandbox. It does not interpret shell syntax.
|
|
141
|
+
- An application Tool can send work through its bound `sandbox` when it needs
|
|
142
|
+
the same file and process restrictions. Its bound `workspace` names paths but
|
|
143
|
+
does not restrict access to them. Do not pass model-selected paths from
|
|
144
|
+
`Workspace#resolve` to `File`; use Sandbox file operations, which reject
|
|
145
|
+
symlinks while opening the path.
|
|
146
|
+
|
|
147
|
+
This distinction keeps the architecture predictable:
|
|
148
|
+
|
|
149
|
+
```text
|
|
150
|
+
model ──arguments──> Tool#call ──> application service
|
|
151
|
+
│
|
|
152
|
+
└──> bound Sandbox ──> file or child process
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Provider requests also leave from the application process. Sandbox network
|
|
156
|
+
settings apply to processes launched through that Sandbox, not to model
|
|
157
|
+
providers, callbacks, or arbitrary Ruby inside a Tool.
|
|
158
|
+
|
|
159
|
+
Read [Workspaces and Sandboxes](sandboxing.md) before exposing filesystem or
|
|
160
|
+
process operations to model influence. It shows which paths and commands a
|
|
161
|
+
child process can access, how networking is restricted, and who cleans up the
|
|
162
|
+
resources.
|
|
163
|
+
|
|
164
|
+
## Use the run-scoped binding
|
|
165
|
+
|
|
166
|
+
LittleGhost creates and binds fresh Tool instances for each Agent run. A Run is
|
|
167
|
+
one top-level Agent or Assembly execution. A Tool can reach that `run`, its
|
|
168
|
+
`agent`, the `runtime` that holds shared configuration and services, and the
|
|
169
|
+
current `workspace` and `sandbox` through accessors supplied by
|
|
170
|
+
`Tool::Binding`.
|
|
171
|
+
|
|
172
|
+
That binding carries application collaborators, not model arguments. Keep request
|
|
173
|
+
identity on `run.invocation`, working state on `context.state`, and the
|
|
174
|
+
model-selected input in the `input` passed to `#call`. Keeping those three
|
|
175
|
+
sources distinct makes permission checks easier to follow.
|
|
176
|
+
|
|
177
|
+
Registries close Tool instances that implement `#close`. Tool instance state
|
|
178
|
+
therefore belongs to one Agent run unless your Tool deliberately talks to a
|
|
179
|
+
shared application service.
|
|
180
|
+
|
|
181
|
+
Use `available_if` when the run itself determines whether an operation exists.
|
|
182
|
+
The predicate receives the same binding and runs before the Tool is constructed:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
class UpdateSlackMessageTool < LittleGhost::Tool
|
|
186
|
+
available_if { |binding| binding.run.invocation.interface == "slack" }
|
|
187
|
+
end
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
This controls discovery, not authorization. The Tool must still validate the
|
|
191
|
+
caller identity and account permissions supplied by the application.
|
|
192
|
+
|
|
193
|
+
## Make concurrency and retries deliberate
|
|
194
|
+
|
|
195
|
+
LittleGhost may run independent Tool calls concurrently. Mark a Tool
|
|
196
|
+
`exclusive true` when it reads or changes shared mutable state that must not
|
|
197
|
+
overlap another exclusive Tool in the same run:
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
class UpdateDraftTool < LittleGhost::Tool
|
|
201
|
+
exclusive true
|
|
202
|
+
description "Replace one section of the current account's draft."
|
|
203
|
+
input_schema(
|
|
204
|
+
type: "object",
|
|
205
|
+
properties: {
|
|
206
|
+
section: {type: "string"},
|
|
207
|
+
content: {type: "string"}
|
|
208
|
+
},
|
|
209
|
+
required: %w[section content],
|
|
210
|
+
additionalProperties: false
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def call(input)
|
|
214
|
+
Drafts.replace_section(
|
|
215
|
+
account_id: run.invocation.context.fetch("account_id"),
|
|
216
|
+
section: input.fetch("section"),
|
|
217
|
+
content: input.fetch("content")
|
|
218
|
+
)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Retries can repeat a Tool call. Prefer read-only operations, idempotency keys,
|
|
224
|
+
or writes that are safe to apply more than once. Do not rely on the prompt to
|
|
225
|
+
prevent duplicate side effects.
|
|
226
|
+
|
|
227
|
+
Raise `LittleGhost::ToolError` for an expected failure the model can act on.
|
|
228
|
+
Its message is model-visible, so keep it safe to disclose. LittleGhost hides
|
|
229
|
+
unexpected exception messages from the model while retaining the original
|
|
230
|
+
error for trusted application inspection.
|
|
231
|
+
|
|
232
|
+
## Return values and artifacts
|
|
233
|
+
|
|
234
|
+
A Tool normally returns one Ruby value. Application callers and code mode
|
|
235
|
+
receive that value, while LittleGhost serializes it for the model. Use
|
|
236
|
+
`Tool::Result` when the operation also produces files or media:
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
def call(input)
|
|
240
|
+
report = Reports.build(input.fetch("period"))
|
|
241
|
+
LittleGhost::Tool::Result.new(
|
|
242
|
+
value: {rows: report.rows.length},
|
|
243
|
+
artifacts: [
|
|
244
|
+
LittleGhost::Artifact.new(
|
|
245
|
+
data: report.csv,
|
|
246
|
+
media_type: "text/csv",
|
|
247
|
+
name: "report.csv"
|
|
248
|
+
)
|
|
249
|
+
]
|
|
250
|
+
)
|
|
251
|
+
end
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`Tool::Result#value` is the same plain Ruby value a Tool would otherwise return.
|
|
255
|
+
Each inline `Artifact` has bytes, a MIME media type, and an optional name and
|
|
256
|
+
metadata. Use `Artifact.deferred(reference:, media_type:, ...)` when another
|
|
257
|
+
application service stores the bytes. The optional block passed to
|
|
258
|
+
`Configuration#artifacts` receives the deferred Artifact and may use its
|
|
259
|
+
application-defined `reference` to load the bytes later.
|
|
260
|
+
|
|
261
|
+
Artifact handling is opt-in for the Runtime:
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
LittleGhost.configure do |config|
|
|
265
|
+
config.workspace = {
|
|
266
|
+
provider: :directory,
|
|
267
|
+
root: "tmp/agent-runs",
|
|
268
|
+
paths: {artifacts: "artifacts"}
|
|
269
|
+
}
|
|
270
|
+
config.artifacts
|
|
271
|
+
end
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Once enabled, artifact handling covers three cases:
|
|
275
|
+
|
|
276
|
+
- Input images and documents are stored so filesystem Tools and code mode can
|
|
277
|
+
read the same bytes. The model still receives only the original image or
|
|
278
|
+
document.
|
|
279
|
+
- Tool artifacts are stored and returned to the model as images or documents.
|
|
280
|
+
The stored reference is shown only when the media cannot be included in the
|
|
281
|
+
model request.
|
|
282
|
+
- Oversized successful Tool values are stored automatically. The model receives
|
|
283
|
+
a short preview and a reference instead of the complete value.
|
|
284
|
+
|
|
285
|
+
The Tool's Ruby return value does not change. LittleGhost limits the size of
|
|
286
|
+
each stored file, the total files and bytes stored for a Run, and the media
|
|
287
|
+
sent in one model turn. If an oversized value cannot be stored within those
|
|
288
|
+
limits, the application still receives the complete Ruby value and the model
|
|
289
|
+
receives a short preview with a storage-limit notice.
|
|
290
|
+
|
|
291
|
+
To load a deferred artifact, pass a block. It may return bytes, an inline
|
|
292
|
+
Artifact with a final MIME type, or `nil` when the referenced file is no longer
|
|
293
|
+
available:
|
|
294
|
+
|
|
295
|
+
```ruby
|
|
296
|
+
config.artifacts do |artifact, run:|
|
|
297
|
+
StoredFiles.read(artifact.reference, actor_id: run.invocation.actor_id)
|
|
298
|
+
end
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
> **Safety note:** A deferred reference is data, not proof that the current
|
|
302
|
+
> caller may read a file. Check it against identity established by the
|
|
303
|
+
> application, restrict the storage service or network destination, and limit
|
|
304
|
+
> the bytes fetched before returning them. LittleGhost applies its storage limit
|
|
305
|
+
> after the resolver returns.
|
|
306
|
+
|
|
307
|
+
Declaring the named Workspace path does not grant model access. When the Agent
|
|
308
|
+
should read artifact references, grant the Sandbox read access to the
|
|
309
|
+
`:artifacts` path and include a filesystem Tool. The model can list that path
|
|
310
|
+
when a task genuinely needs a stored file; ordinary multimodal work does not
|
|
311
|
+
need a second reference to media already in the conversation.
|
|
312
|
+
|
|
313
|
+
## Let code mode compose the same capabilities
|
|
314
|
+
|
|
315
|
+
Without code mode, the model chooses one Tool operation and LittleGhost returns
|
|
316
|
+
the result before the model chooses the next step. Code mode lets the model
|
|
317
|
+
write a small Ruby program that calls several of the same Tools, combines their
|
|
318
|
+
results, and returns one useful value.
|
|
319
|
+
|
|
320
|
+
Each call crosses back to the parent Ruby process. LittleGhost checks the schema
|
|
321
|
+
and calls the Tool method there, so permission checks inside `#call` still
|
|
322
|
+
apply. Code mode receives the Tool's Ruby return value, while artifacts return
|
|
323
|
+
with the surrounding `exec` or `wait` result. Tool limits, callbacks, events,
|
|
324
|
+
and tracing work the same way for direct and code-mode calls.
|
|
325
|
+
|
|
326
|
+
Continue with [Structured Results and Content](structured_outputs_and_content.md)
|
|
327
|
+
to give Agent responses a predictable shape and accept images or documents.
|
|
328
|
+
For exact Tool DSL and result contracts, see `LittleGhost::Tool` and
|
|
329
|
+
`LittleGhost::Tool::Binding`.
|
|
@@ -18,13 +18,13 @@ module LittleGhost
|
|
|
18
18
|
# The adapter has no state between #stream calls, so one instance can
|
|
19
19
|
# translate independent runs.
|
|
20
20
|
#
|
|
21
|
-
# ===
|
|
21
|
+
# === Choose what the interface receives
|
|
22
22
|
#
|
|
23
23
|
# Provider plaintext reasoning becomes AG-UI reasoning events. Tool
|
|
24
24
|
# arguments and results, invocation metadata, subagent events, trace context,
|
|
25
25
|
# and selected error text also pass through without redaction. Authorize and
|
|
26
26
|
# filter the complete stream before transport, and send it only to an
|
|
27
|
-
# interface
|
|
27
|
+
# interface intended to display that data. Encrypted reasoning and provider
|
|
28
28
|
# continuity artifacts are never exposed here.
|
|
29
29
|
class Adapter
|
|
30
30
|
TERMINAL_EVENTS = %i[run_partial run_cancel run_stop run_error].freeze # :nodoc:
|
|
@@ -22,7 +22,7 @@ module LittleGhost
|
|
|
22
22
|
#
|
|
23
23
|
# Tool overrides must be classes. A delegated agent otherwise receives only
|
|
24
24
|
# its own declared tools; it does not inherit the parent's registry. The
|
|
25
|
-
# manager enforces
|
|
25
|
+
# manager enforces concurrency, identity, wait duration, and persistence bounds.
|
|
26
26
|
module Delegation
|
|
27
27
|
def self.included(base) # :nodoc:
|
|
28
28
|
base.extend(ClassMethods)
|
|
@@ -40,7 +40,7 @@ module LittleGhost
|
|
|
40
40
|
# subagent_long_poll_duration() -> Float
|
|
41
41
|
# subagent_long_poll_duration(seconds) -> Float
|
|
42
42
|
#
|
|
43
|
-
#
|
|
43
|
+
# How long +wait_for_subagents+ watches for progress before returning.
|
|
44
44
|
#
|
|
45
45
|
# The default comes from Subagents::Manager. Values must be positive,
|
|
46
46
|
# finite numbers and are normalized to Float.
|
|
@@ -36,7 +36,12 @@ module LittleGhost
|
|
|
36
36
|
end
|
|
37
37
|
paths ||= binding.run.runtime.skill_paths
|
|
38
38
|
catalog = LittleGhost::Skills::Catalog.new(
|
|
39
|
-
**configuration.merge(
|
|
39
|
+
**configuration.merge(
|
|
40
|
+
paths:,
|
|
41
|
+
resource_root: binding.run&.runtime&.skill_resource_root,
|
|
42
|
+
workspace: binding.workspace,
|
|
43
|
+
sandbox: binding.sandbox
|
|
44
|
+
)
|
|
40
45
|
)
|
|
41
46
|
return [] if catalog.names.empty?
|
|
42
47
|
|
|
@@ -110,6 +110,7 @@ module LittleGhost
|
|
|
110
110
|
|
|
111
111
|
def track_tool_call(payload, context: nil)
|
|
112
112
|
tool_use = payload.fetch(:tool_use)
|
|
113
|
+
return Support::Callbacks.continue if code_mode_control_tool?(tool_use)
|
|
113
114
|
return Support::Callbacks.continue if @tool_loop_except.include?(tool_use.name)
|
|
114
115
|
|
|
115
116
|
key = [tool_use.name, tool_loop_digest(tool_use.input)]
|
|
@@ -173,11 +174,14 @@ module LittleGhost
|
|
|
173
174
|
tool_name: tool_use.name,
|
|
174
175
|
count: count
|
|
175
176
|
)
|
|
177
|
+
warned_content = "#{warning}\n\n#{result.content}"
|
|
176
178
|
replacement = Tool::ExecutionResult.new(
|
|
177
|
-
|
|
179
|
+
value: result.value,
|
|
180
|
+
content: warned_content,
|
|
178
181
|
status: result.status,
|
|
179
182
|
error: result.error,
|
|
180
|
-
|
|
183
|
+
artifacts: result.artifacts,
|
|
184
|
+
presentation_content: result.presentation_content
|
|
181
185
|
)
|
|
182
186
|
Support::Callbacks.replace(payload.merge(result: replacement))
|
|
183
187
|
end
|