little_ghost 0.4.0 → 0.6.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 (124) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +28 -6
  3. data/docs/guides/assemblies.md +143 -17
  4. data/docs/guides/code_mode.md +276 -0
  5. data/docs/guides/core_concepts.md +46 -30
  6. data/docs/guides/getting_started.md +17 -6
  7. data/docs/guides/integrations.md +217 -0
  8. data/docs/guides/models_and_providers.md +125 -0
  9. data/docs/guides/production.md +190 -17
  10. data/docs/guides/prompt_views.md +14 -7
  11. data/docs/guides/sandboxing.md +282 -0
  12. data/docs/guides/skills.md +141 -0
  13. data/docs/guides/structured_outputs_and_content.md +135 -0
  14. data/docs/guides/tools.md +355 -0
  15. data/lib/little_ghost/ag_ui/adapter.rb +2 -2
  16. data/lib/little_ghost/agent/delegation.rb +2 -2
  17. data/lib/little_ghost/agent/skills.rb +6 -1
  18. data/lib/little_ghost/agent/tool_loop.rb +6 -2
  19. data/lib/little_ghost/agent.rb +250 -48
  20. data/lib/little_ghost/agent_builder.rb +61 -22
  21. data/lib/little_ghost/agent_stream_source.rb +262 -0
  22. data/lib/little_ghost/artifact.rb +182 -0
  23. data/lib/little_ghost/artifacts/presentation_budget.rb +44 -0
  24. data/lib/little_ghost/artifacts/workspace_store.rb +370 -0
  25. data/lib/little_ghost/artifacts.rb +11 -0
  26. data/lib/little_ghost/assembly.rb +64 -20
  27. data/lib/little_ghost/assembly_builder.rb +26 -26
  28. data/lib/little_ghost/assembly_execution.rb +23 -11
  29. data/lib/little_ghost/code_mode/broker.rb +168 -0
  30. data/lib/little_ghost/code_mode/catalog.rb +54 -0
  31. data/lib/little_ghost/code_mode/engine.rb +58 -0
  32. data/lib/little_ghost/code_mode/javascript/catalog.rb +118 -0
  33. data/lib/little_ghost/code_mode/javascript/client.rb +550 -0
  34. data/lib/little_ghost/code_mode/javascript/host.rb +347 -0
  35. data/lib/little_ghost/code_mode/javascript/session.rb +578 -0
  36. data/lib/little_ghost/code_mode/javascript_engine.rb +172 -0
  37. data/lib/little_ghost/code_mode/protocol.rb +78 -0
  38. data/lib/little_ghost/code_mode/ruby/catalog.rb +80 -0
  39. data/lib/little_ghost/code_mode/ruby/host.rb +102 -0
  40. data/lib/little_ghost/code_mode/ruby/session.rb +508 -0
  41. data/lib/little_ghost/code_mode/ruby_engine.rb +86 -0
  42. data/lib/little_ghost/code_mode/runtime.rb +335 -0
  43. data/lib/little_ghost/code_mode/session.rb +61 -0
  44. data/lib/little_ghost/code_mode/types.rb +58 -0
  45. data/lib/little_ghost/code_mode.rb +53 -0
  46. data/lib/little_ghost/configuration.rb +208 -21
  47. data/lib/little_ghost/content.rb +19 -8
  48. data/lib/little_ghost/data_map.rb +2 -2
  49. data/lib/little_ghost/errors.rb +26 -7
  50. data/lib/little_ghost/execution.rb +32 -27
  51. data/lib/little_ghost/execution_state.rb +3 -3
  52. data/lib/little_ghost/graph.rb +373 -210
  53. data/lib/little_ghost/instrumentation.rb +2 -1
  54. data/lib/little_ghost/mcp/client.rb +487 -88
  55. data/lib/little_ghost/mcp/toolset.rb +210 -0
  56. data/lib/little_ghost/mcp/types.rb +216 -0
  57. data/lib/little_ghost/mcp.rb +3 -0
  58. data/lib/little_ghost/model_capabilities.rb +9 -6
  59. data/lib/little_ghost/model_request.rb +0 -12
  60. data/lib/little_ghost/model_resolver.rb +13 -3
  61. data/lib/little_ghost/model_response.rb +3 -7
  62. data/lib/little_ghost/network/authorizer_server.rb +162 -0
  63. data/lib/little_ghost/network/certificate_authority.rb +104 -0
  64. data/lib/little_ghost/network/envoy_config.rb +362 -0
  65. data/lib/little_ghost/network/envoy_gateway.rb +418 -0
  66. data/lib/little_ghost/network/external_gateway.rb +68 -0
  67. data/lib/little_ghost/network.rb +96 -0
  68. data/lib/little_ghost/prompt_resolver.rb +7 -7
  69. data/lib/little_ghost/provider_registry.rb +3 -3
  70. data/lib/little_ghost/providers/anthropic.rb +8 -1
  71. data/lib/little_ghost/providers/gemini.rb +10 -1
  72. data/lib/little_ghost/providers/vertex_ai.rb +6 -1
  73. data/lib/little_ghost/run.rb +89 -25
  74. data/lib/little_ghost/run_context.rb +11 -1
  75. data/lib/little_ghost/run_result.rb +0 -7
  76. data/lib/little_ghost/runtime/hook.rb +5 -0
  77. data/lib/little_ghost/runtime/hooks/artifacts.rb +338 -0
  78. data/lib/little_ghost/runtime.rb +111 -34
  79. data/lib/little_ghost/sandbox/capabilities.rb +68 -0
  80. data/lib/little_ghost/sandbox/environment_policy.rb +41 -0
  81. data/lib/little_ghost/sandbox/filesystem.rb +377 -0
  82. data/lib/little_ghost/sandbox/isolated_backend.rb +163 -0
  83. data/lib/little_ghost/sandbox/limits.rb +48 -0
  84. data/lib/little_ghost/sandbox/mount.rb +129 -0
  85. data/lib/little_ghost/sandbox/network_policy.rb +108 -0
  86. data/lib/little_ghost/sandbox/policy.rb +88 -0
  87. data/lib/little_ghost/sandbox/process_runner.rb +103 -0
  88. data/lib/little_ghost/sandbox/process_session.rb +335 -0
  89. data/lib/little_ghost/sandbox/scope.rb +304 -0
  90. data/lib/little_ghost/sandbox.rb +158 -32
  91. data/lib/little_ghost/sandboxes/bubblewrap.rb +351 -0
  92. data/lib/little_ghost/sandboxes/native.rb +51 -0
  93. data/lib/little_ghost/sandboxes/seatbelt.rb +261 -0
  94. data/lib/little_ghost/sandboxes/unrestricted.rb +240 -0
  95. data/lib/little_ghost/session.rb +30 -6
  96. data/lib/little_ghost/session_stores/filesystem.rb +29 -13
  97. data/lib/little_ghost/skills/catalog.rb +94 -14
  98. data/lib/little_ghost/skills/resource_root.rb +42 -0
  99. data/lib/little_ghost/skills/skill.rb +0 -3
  100. data/lib/little_ghost/stream_event.rb +8 -13
  101. data/lib/little_ghost/subagents/control_tool.rb +8 -0
  102. data/lib/little_ghost/subagents/manager.rb +182 -79
  103. data/lib/little_ghost/support/callbacks.rb +3 -1
  104. data/lib/little_ghost/support/cancellation_token.rb +2 -2
  105. data/lib/little_ghost/support/content_capture.rb +3 -3
  106. data/lib/little_ghost/support/executor.rb +84 -15
  107. data/lib/little_ghost/support/http_client.rb +11 -4
  108. data/lib/little_ghost/support/interruptible_stream.rb +4 -2
  109. data/lib/little_ghost/support/pooled_thread_runner.rb +126 -0
  110. data/lib/little_ghost/support/redactor.rb +1 -1
  111. data/lib/little_ghost/support/serialized_dispatcher.rb +84 -0
  112. data/lib/little_ghost/support/task.rb +126 -0
  113. data/lib/little_ghost/support/task_runner.rb +74 -0
  114. data/lib/little_ghost/support.rb +4 -0
  115. data/lib/little_ghost/tool.rb +104 -53
  116. data/lib/little_ghost/tool_registry.rb +1 -1
  117. data/lib/little_ghost/tools/filesystem.rb +1 -1
  118. data/lib/little_ghost/tools/shell.rb +4 -3
  119. data/lib/little_ghost/version.rb +1 -1
  120. data/lib/little_ghost/workflow.rb +3 -3
  121. data/lib/little_ghost/workspace.rb +222 -8
  122. data/lib/little_ghost.rb +38 -12
  123. metadata +116 -2
  124. data/lib/little_ghost/unrestricted_sandbox.rb +0 -306
@@ -0,0 +1,141 @@
1
+ # Give Agents Skills
2
+
3
+ A Skill is a reusable instruction package that an Agent can discover by name
4
+ and load when a task needs it. Skills keep specialized procedures out of the
5
+ Agent's main prompt.
6
+
7
+ ## Write one focused skill
8
+
9
+ Create `app/skills/refund_guide/SKILL.md`:
10
+
11
+ ```markdown
12
+ ---
13
+ name: refund_guide
14
+ description: Determine which refund guidance applies to a customer request.
15
+ allowed-tools:
16
+ - help_center_lookup
17
+ compatibility: Requires the refunds help-center collection.
18
+ ---
19
+
20
+ # Apply the refund guidance
21
+
22
+ 1. Load the current refund entry with `help_center_lookup`.
23
+ 2. Compare the purchase date and item category with the returned guidance.
24
+ 3. State which facts support the decision and which facts are still missing.
25
+ 4. Return the analysis to the application instead of issuing a refund.
26
+ ```
27
+
28
+ Each immediate child of a configured skill root may contain one `SKILL.md`.
29
+ Front matter requires a single-line `name` and `description`. Names contain
30
+ only letters, numbers, underscores, and hyphens. `allowed-tools` and
31
+ `compatibility` are optional information shown to the model.
32
+
33
+ A Skill may also include supporting files in `references/`, `scripts/`, or
34
+ `assets/`. Keep the main instructions short and point to a resource only when
35
+ the task needs it.
36
+
37
+ ## Let an Agent discover skills
38
+
39
+ The conventional root is `app/skills`:
40
+
41
+ ```ruby
42
+ class CustomerSupportAgent < LittleGhost::Agent
43
+ system_prompt "Use an available skill when a request needs a documented procedure."
44
+ tools HelpCenterLookupTool
45
+ skills
46
+ end
47
+ ```
48
+
49
+ At runtime, the Agent receives a short catalog of skill names, descriptions,
50
+ and locations. The generated `skills` Tool loads the full instructions when
51
+ the model selects one. An empty catalog adds nothing to the prompt and no Tool.
52
+
53
+ You can use another application-owned directory or expose only selected
54
+ skills:
55
+
56
+ ```ruby
57
+ class CustomerSupportAgent < LittleGhost::Agent
58
+ skills paths: [File.expand_path("../../support_skills", __dir__)],
59
+ only: %w[refund_guide]
60
+ end
61
+ ```
62
+
63
+ Paths may also come from a callable resolved for each Run. Build the path from
64
+ application configuration, not from a path supplied by the model or request.
65
+
66
+ ## Pair instructions with Tools
67
+
68
+ `allowed-tools` tells the model which Tools a Skill expects. It doesn't add or
69
+ enable those Tools. The Agent's `tools` declaration remains the source of what
70
+ the model can call, and each Tool applies its normal application checks.
71
+
72
+ This separation makes Skills safe to use as guidance: changing a Markdown file
73
+ can change what the Agent tries, but it can't grant a new Tool, provider,
74
+ filesystem path, or credential.
75
+
76
+ > **Safety note:** Keep configured skill directories under application control
77
+ > and review Skill changes as prompt changes. A Skill can contain mistaken or
78
+ > outdated instructions, so application code should continue to check any
79
+ > operation that has side effects.
80
+
81
+ ## Make resources available to code mode
82
+
83
+ Ordinary Skill loading needs no Workspace configuration. If model-authored code
84
+ also needs to read Skill resources, give those resources a stable model-visible
85
+ location:
86
+
87
+ ```ruby
88
+ require "fileutils"
89
+ require "tmpdir"
90
+
91
+ skill_root = File.expand_path("../../app/skills", __dir__)
92
+
93
+ LittleGhost.configure do |config|
94
+ config.skill_paths = [skill_root]
95
+ config.skill_resource_root = "workspace://skills"
96
+
97
+ config.workspace = lambda do |**|
98
+ root = Dir.mktmpdir("little-ghost-skills-")
99
+ LittleGhost::Workspace.new(
100
+ root:,
101
+ paths: {skills: skill_root},
102
+ teardown: lambda do |workspace:, **|
103
+ FileUtils.remove_entry_secure(workspace.root) if File.exist?(workspace.root)
104
+ end
105
+ )
106
+ end
107
+
108
+ config.sandbox = {
109
+ provider: :native,
110
+ files: {root: :read_write, skills: :read_only},
111
+ root_filesystem: :isolated,
112
+ network: :none
113
+ }
114
+ end
115
+ ```
116
+
117
+ The named Workspace path maps the configured Skill directory, and the Sandbox
118
+ makes it readable without allowing model-authored code to change it. The model
119
+ can then ask the Filesystem Tool for a path such as
120
+ `workspace://skills/refund_guide/references/example.md` without learning the
121
+ host's directory layout. [Workspaces and Sandboxes](sandboxing.md) explains how
122
+ to adapt the temporary root, backend, and other grants for your application.
123
+
124
+ ## Write instructions that stand on their own
125
+
126
+ A useful Skill tells the Agent:
127
+
128
+ - When the procedure applies.
129
+ - Which information it needs before deciding.
130
+ - Which named Tools or resources help.
131
+ - What result the caller expects.
132
+ - Which action remains with the application.
133
+ - What to do when information is missing or conflicts.
134
+
135
+ Use direct instructions and stable domain language. Avoid repeating the
136
+ Agent's general prompt or relying on Tool behavior that the Tool description
137
+ doesn't promise.
138
+
139
+ Continue with [Workspaces and Sandboxes](sandboxing.md) when model-authored code
140
+ needs Skill resources. See [Tools](tools.md) for Tool validation, bindings, and
141
+ side effects.
@@ -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,355 @@
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
+ When LittleGhost selects the fiber backend, concurrent Tool calls can run as
224
+ fibers on one thread. If a Tool calls a library that blocks that thread, every
225
+ other fiber on it must wait too. Suppose the help center lookup later moves to
226
+ a client whose `lookup` method is documented to behave this way. Change only
227
+ the Tool method:
228
+
229
+ ```ruby
230
+ def call(input)
231
+ LittleGhost.offload_blocking do
232
+ HelpCenterClient.lookup(input.fetch("topic"))
233
+ end
234
+ end
235
+ ```
236
+
237
+ Many Ruby I/O calls already let the scheduler run other fibers. Keep those
238
+ calls unchanged. Use `offload_blocking` only when documentation or measurement
239
+ shows that the exact call pauses other fibers and the work can continue on
240
+ another Ruby thread. Configure the call's own timeout or cancellation when it
241
+ provides one.
242
+
243
+ If most of a Tool's implementation blocks, configure LittleGhost to use the
244
+ `:thread` backend instead of wrapping each call. `exclusive true` prevents
245
+ overlap with another exclusive Tool; it does not change where the Tool runs.
246
+ [Running in Production](production.md#use-an-existing-fiber-scheduler) explains
247
+ the concurrency settings and when to adjust the shared thread pool.
248
+
249
+ Retries can repeat a Tool call. Prefer read-only operations, idempotency keys,
250
+ or writes that are safe to apply more than once. Do not rely on the prompt to
251
+ prevent duplicate side effects.
252
+
253
+ Raise `LittleGhost::ToolError` for an expected failure the model can act on.
254
+ Its message is model-visible, so keep it safe to disclose. LittleGhost hides
255
+ unexpected exception messages from the model while retaining the original
256
+ error for trusted application inspection.
257
+
258
+ ## Return values and artifacts
259
+
260
+ A Tool normally returns one Ruby value. Application callers and code mode
261
+ receive that value, while LittleGhost serializes it for the model. Use
262
+ `Tool::Result` when the operation also produces files or media:
263
+
264
+ ```ruby
265
+ def call(input)
266
+ report = Reports.build(input.fetch("period"))
267
+ LittleGhost::Tool::Result.new(
268
+ value: {rows: report.rows.length},
269
+ artifacts: [
270
+ LittleGhost::Artifact.new(
271
+ data: report.csv,
272
+ media_type: "text/csv",
273
+ name: "report.csv"
274
+ )
275
+ ]
276
+ )
277
+ end
278
+ ```
279
+
280
+ `Tool::Result#value` is the same plain Ruby value a Tool would otherwise return.
281
+ Each inline `Artifact` has bytes, a MIME media type, and an optional name and
282
+ metadata. Use `Artifact.deferred(reference:, media_type:, ...)` when another
283
+ application service stores the bytes. The optional block passed to
284
+ `Configuration#artifacts` receives the deferred Artifact and may use its
285
+ application-defined `reference` to load the bytes later.
286
+
287
+ Artifact handling is opt-in for the Runtime:
288
+
289
+ ```ruby
290
+ LittleGhost.configure do |config|
291
+ config.workspace = {
292
+ provider: :directory,
293
+ root: "tmp/agent-runs",
294
+ paths: {artifacts: "artifacts"}
295
+ }
296
+ config.artifacts
297
+ end
298
+ ```
299
+
300
+ Once enabled, artifact handling covers three cases:
301
+
302
+ - Input images and documents are stored so filesystem Tools and code mode can
303
+ read the same bytes. The model still receives only the original image or
304
+ document.
305
+ - Tool artifacts are stored and returned to the model as images or documents.
306
+ The stored reference is shown only when the media cannot be included in the
307
+ model request.
308
+ - Oversized successful Tool values are stored automatically. The model receives
309
+ a short preview and a reference instead of the complete value.
310
+
311
+ The Tool's Ruby return value does not change. LittleGhost limits the size of
312
+ each stored file, the total files and bytes stored for a Run, and the media
313
+ sent in one model turn. If an oversized value cannot be stored within those
314
+ limits, the application still receives the complete Ruby value and the model
315
+ receives a short preview with a storage-limit notice.
316
+
317
+ To load a deferred artifact, pass a block. It may return bytes, an inline
318
+ Artifact with a final MIME type, or `nil` when the referenced file is no longer
319
+ available:
320
+
321
+ ```ruby
322
+ config.artifacts do |artifact, run:|
323
+ StoredFiles.read(artifact.reference, actor_id: run.invocation.actor_id)
324
+ end
325
+ ```
326
+
327
+ > **Safety note:** A deferred reference is data, not proof that the current
328
+ > caller may read a file. Check it against identity established by the
329
+ > application, restrict the storage service or network destination, and limit
330
+ > the bytes fetched before returning them. LittleGhost applies its storage limit
331
+ > after the resolver returns.
332
+
333
+ Declaring the named Workspace path does not grant model access. When the Agent
334
+ should read artifact references, grant the Sandbox read access to the
335
+ `:artifacts` path and include a filesystem Tool. The model can list that path
336
+ when a task genuinely needs a stored file; ordinary multimodal work does not
337
+ need a second reference to media already in the conversation.
338
+
339
+ ## Let code mode compose the same capabilities
340
+
341
+ Without code mode, the model chooses one Tool operation and LittleGhost returns
342
+ the result before the model chooses the next step. Code mode lets the model
343
+ write a small Ruby program that calls several of the same Tools, combines their
344
+ results, and returns one useful value.
345
+
346
+ Each call crosses back to the parent Ruby process. LittleGhost checks the schema
347
+ and calls the Tool method there, so permission checks inside `#call` still
348
+ apply. Code mode receives the Tool's Ruby return value, while artifacts return
349
+ with the surrounding `exec` or `wait` result. Tool limits, callbacks, events,
350
+ and tracing work the same way for direct and code-mode calls.
351
+
352
+ Continue with [Structured Results and Content](structured_outputs_and_content.md)
353
+ to give Agent responses a predictable shape and accept images or documents.
354
+ For exact Tool DSL and result contracts, see `LittleGhost::Tool` and
355
+ `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
- # === Security and trust
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 trusted to display that data. Encrypted reasoning and provider
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 its concurrency, identity, polling, and persistence bounds.
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
- # The maximum long-poll duration used by subagent wait tools.
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(paths:, resource_root: binding.run&.runtime&.skill_resource_root)
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