robot_lab 0.2.1 → 0.2.6
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/.envrc +1 -0
- data/.loki +60 -0
- data/.quality/reek_baseline.txt +43 -0
- data/.rubocop.yml +5 -167
- data/CHANGELOG.md +54 -0
- data/README.md +61 -0
- data/Rakefile +28 -12
- data/docs/api/core/index.md +1 -0
- data/docs/api/core/network.md +31 -1
- data/docs/api/core/robot.md +91 -5
- data/docs/api/errors.md +47 -2
- data/docs/api/index.md +3 -0
- data/docs/architecture/core-concepts.md +40 -0
- data/docs/architecture/network-orchestration.md +8 -0
- data/docs/architecture/robot-execution.md +1 -1
- data/docs/getting-started/configuration.md +20 -1
- data/docs/guides/building-robots.md +39 -1
- data/docs/guides/creating-networks.md +6 -1
- data/docs/guides/hooks.md +997 -0
- data/docs/guides/index.md +5 -0
- data/docs/guides/knowledge.md +2 -2
- data/docs/guides/observability.md +74 -7
- data/docs/guides/using-tools.md +69 -0
- data/docs/index.md +1 -1
- data/examples/03_network.rb +11 -2
- data/examples/34_agentskills.rb +0 -0
- data/examples/35_hooks.rb +256 -0
- data/examples/README.md +7 -0
- data/examples/common.rb +7 -3
- data/examples/xyzzy.rb +90 -0
- data/lib/robot_lab/agent_skill.rb +5 -4
- data/lib/robot_lab/budget/ledger.rb +98 -0
- data/lib/robot_lab/capabilities.rb +84 -0
- data/lib/robot_lab/config/defaults.yml +10 -0
- data/lib/robot_lab/config.rb +4 -4
- data/lib/robot_lab/error.rb +34 -4
- data/lib/robot_lab/errors.rb +45 -0
- data/lib/robot_lab/hook.rb +79 -0
- data/lib/robot_lab/hook_context.rb +194 -0
- data/lib/robot_lab/hook_registry.rb +55 -0
- data/lib/robot_lab/hooks.rb +87 -0
- data/lib/robot_lab/mcp/connection_poller.rb +2 -2
- data/lib/robot_lab/narrator.rb +87 -0
- data/lib/robot_lab/network.rb +77 -20
- data/lib/robot_lab/robot/budget.rb +89 -0
- data/lib/robot_lab/robot/bus_messaging.rb +76 -12
- data/lib/robot_lab/robot/hooking.rb +56 -0
- data/lib/robot_lab/robot/mcp_management.rb +8 -2
- data/lib/robot_lab/robot/template_rendering.rb +2 -2
- data/lib/robot_lab/robot.rb +211 -88
- data/lib/robot_lab/run_config.rb +24 -5
- data/lib/robot_lab/runnable.rb +51 -0
- data/lib/robot_lab/sandbox/null.rb +13 -0
- data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
- data/lib/robot_lab/sandbox.rb +52 -0
- data/lib/robot_lab/script_tool.rb +60 -2
- data/lib/robot_lab/task.rb +26 -20
- data/lib/robot_lab/tool.rb +52 -11
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab.rb +49 -0
- data/mkdocs.yml +1 -0
- metadata +36 -99
- data/examples/temp.md +0 -51
- data/site/404.html +0 -2300
- data/site/api/core/index.html +0 -2706
- data/site/api/core/memory/index.html +0 -3793
- data/site/api/core/network/index.html +0 -3500
- data/site/api/core/robot/index.html +0 -4566
- data/site/api/core/state/index.html +0 -3390
- data/site/api/core/tool/index.html +0 -3843
- data/site/api/index.html +0 -2635
- data/site/api/mcp/client/index.html +0 -3435
- data/site/api/mcp/index.html +0 -2783
- data/site/api/mcp/server/index.html +0 -3252
- data/site/api/mcp/transports/index.html +0 -3352
- data/site/api/messages/index.html +0 -2641
- data/site/api/messages/text-message/index.html +0 -3087
- data/site/api/messages/tool-call-message/index.html +0 -3159
- data/site/api/messages/tool-result-message/index.html +0 -3252
- data/site/api/messages/user-message/index.html +0 -3212
- data/site/api/streaming/context/index.html +0 -3282
- data/site/api/streaming/events/index.html +0 -3347
- data/site/api/streaming/index.html +0 -2738
- data/site/architecture/core-concepts/index.html +0 -3757
- data/site/architecture/index.html +0 -2797
- data/site/architecture/message-flow/index.html +0 -3238
- data/site/architecture/network-orchestration/index.html +0 -3433
- data/site/architecture/robot-execution/index.html +0 -3140
- data/site/architecture/state-management/index.html +0 -3498
- data/site/assets/css/custom.css +0 -56
- data/site/assets/images/favicon.png +0 -0
- data/site/assets/images/robot_lab.jpg +0 -0
- data/site/assets/javascripts/bundle.79ae519e.min.js +0 -16
- data/site/assets/javascripts/bundle.79ae519e.min.js.map +0 -7
- data/site/assets/javascripts/lunr/min/lunr.ar.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.da.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.de.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.du.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.el.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.es.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.fi.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.fr.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.he.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.hi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.hu.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.hy.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.it.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ja.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.jp.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.kn.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.ko.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.multi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.nl.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.no.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.pt.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ro.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ru.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.sa.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.sv.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.ta.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.te.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.th.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.tr.min.js +0 -18
- data/site/assets/javascripts/lunr/min/lunr.vi.min.js +0 -1
- data/site/assets/javascripts/lunr/min/lunr.zh.min.js +0 -1
- data/site/assets/javascripts/lunr/tinyseg.js +0 -206
- data/site/assets/javascripts/lunr/wordcut.js +0 -6708
- data/site/assets/javascripts/workers/search.2c215733.min.js +0 -42
- data/site/assets/javascripts/workers/search.2c215733.min.js.map +0 -7
- data/site/assets/stylesheets/main.484c7ddc.min.css +0 -1
- data/site/assets/stylesheets/main.484c7ddc.min.css.map +0 -1
- data/site/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
- data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +0 -1
- data/site/concepts/index.html +0 -3455
- data/site/examples/basic-chat/index.html +0 -2880
- data/site/examples/index.html +0 -2907
- data/site/examples/mcp-server/index.html +0 -3018
- data/site/examples/multi-robot-network/index.html +0 -3131
- data/site/examples/rails-application/index.html +0 -3329
- data/site/examples/tool-usage/index.html +0 -3085
- data/site/getting-started/configuration/index.html +0 -3745
- data/site/getting-started/index.html +0 -2572
- data/site/getting-started/installation/index.html +0 -2981
- data/site/getting-started/quick-start/index.html +0 -2942
- data/site/guides/building-robots/index.html +0 -4290
- data/site/guides/creating-networks/index.html +0 -3858
- data/site/guides/index.html +0 -2586
- data/site/guides/mcp-integration/index.html +0 -3581
- data/site/guides/memory/index.html +0 -3586
- data/site/guides/rails-integration/index.html +0 -4019
- data/site/guides/streaming/index.html +0 -3157
- data/site/guides/using-tools/index.html +0 -3802
- data/site/index.html +0 -2671
- data/site/search/search_index.json +0 -1
- data/site/sitemap.xml +0 -183
- data/site/sitemap.xml.gz +0 -0
- data/site/tags.json +0 -1
data/docs/guides/index.md
CHANGED
|
@@ -42,6 +42,10 @@ If you're new to RobotLab, start here:
|
|
|
42
42
|
|
|
43
43
|
Token tracking, circuit breakers, doom loop detection, auto compaction, and learning accumulation
|
|
44
44
|
|
|
45
|
+
- [:octicons-zap-24: **Hook System**](hooks.md)
|
|
46
|
+
|
|
47
|
+
Lifecycle hooks for instrumentation, response caching, extensions, and cross-cutting concerns
|
|
48
|
+
|
|
45
49
|
- [:octicons-search-24: **Knowledge & Retrieval**](knowledge.md)
|
|
46
50
|
|
|
47
51
|
Chat history search and embedding-based document store for RAG workflows
|
|
@@ -59,6 +63,7 @@ If you're new to RobotLab, start here:
|
|
|
59
63
|
| [Streaming](streaming.md) | Real-time responses | 5 min |
|
|
60
64
|
| [Memory](memory.md) | Shared data store | 5 min |
|
|
61
65
|
| [Observability & Safety](observability.md) | Token tracking, circuit breaker, doom loop detection, auto compaction, learning loop | 10 min |
|
|
66
|
+
| [Hook System](hooks.md) | Lifecycle hooks, extensions, response caching, instrumentation | 15 min |
|
|
62
67
|
| [Knowledge & Retrieval](knowledge.md) | Chat history search and embedding-based document store (RAG) | 10 min |
|
|
63
68
|
|
|
64
69
|
## Extension Gems
|
data/docs/guides/knowledge.md
CHANGED
|
@@ -184,5 +184,5 @@ This gem bundles `fastembed` for ONNX-based embeddings. The `BAAI/bge-small-en-v
|
|
|
184
184
|
## See Also
|
|
185
185
|
|
|
186
186
|
- [Observability Guide](observability.md)
|
|
187
|
-
- [
|
|
188
|
-
- [
|
|
187
|
+
- [examples/25_history_search.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/25_history_search.rb) — Chat History Search
|
|
188
|
+
- [examples/26_document_store.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/26_document_store.rb) — Embedding-Based Document Store
|
|
@@ -10,6 +10,7 @@ Facilities that help you monitor, control, improve, and scale robot behaviour:
|
|
|
10
10
|
- **Context Window Compression** — prune irrelevant history to stay within token budgets
|
|
11
11
|
- **Convergence Detection** — detect when independent agents reach the same conclusion
|
|
12
12
|
- **Structured Delegation** — synchronous inter-robot calls with duration and token metadata
|
|
13
|
+
- **Live Narration** — an opt-in console feed of what a robot is doing as it happens
|
|
13
14
|
|
|
14
15
|
---
|
|
15
16
|
|
|
@@ -91,6 +92,38 @@ result = fresh.run("Explain memoization.")
|
|
|
91
92
|
puts result.input_tokens # smallest possible — no prior history
|
|
92
93
|
```
|
|
93
94
|
|
|
95
|
+
### Budgets (Token & Cost)
|
|
96
|
+
|
|
97
|
+
Where token/cost tracking above is purely observational, `token_budget:` and `cost_budget:` make it enforceable — a `Robot` refuses to keep spending once a configured limit is reached:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
robot = RobotLab.build(
|
|
101
|
+
name: "capped",
|
|
102
|
+
system_prompt: "You are a concise assistant.",
|
|
103
|
+
token_budget: 10_000, # cumulative input + output tokens
|
|
104
|
+
cost_budget: 0.50 # cumulative $ across all runs
|
|
105
|
+
)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Enforcement happens in two layers, backed by a thread-safe `RobotLab::Budget::Ledger`:
|
|
109
|
+
|
|
110
|
+
- **Before the call** — `run()` reserves whatever remains of each configured dimension. If a *prior* call already exhausted the budget, the reservation raises `RobotLab::BudgetExceeded` immediately, refusing the call outright before spending anything on it.
|
|
111
|
+
- **After the call** — actual usage (tokens from the result, cost from the response, when the provider reports pricing) replaces the reservation. If *this* call's actual usage pushes cumulative usage over budget, `RobotLab::InferenceError` is raised (unavoidable for the call that causes the overage, since totals aren't known until the response comes back) — the same error `token_budget` alone has always raised.
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
begin
|
|
115
|
+
robot.run("Another expensive task")
|
|
116
|
+
rescue RobotLab::BudgetExceeded => e
|
|
117
|
+
puts e.message # "budget exceeded for cost: 0.51 > 0.5" — refused before spending
|
|
118
|
+
rescue RobotLab::InferenceError => e
|
|
119
|
+
puts e.message # "Cost budget exceeded: $0.5231 used, budget is $0.5" — this call pushed it over
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
A dimension with no configured limit (e.g. `cost_budget` when only `token_budget` is set) is treated as unlimited and never raises. Both fields are also available on `RunConfig` and cascade through the same global → network → robot hierarchy as other infrastructure fields (see [RunConfig](../getting-started/configuration.md#runconfig-shared-operational-defaults)).
|
|
124
|
+
|
|
125
|
+
This is a native alternative to the hand-rolled `BudgetHook` pattern in the [Hooks guide](hooks.md#cost-enforcement) for the common case of a per-robot token or dollar ceiling; reach for a hook instead when you need cross-robot session totals or custom accounting.
|
|
126
|
+
|
|
94
127
|
---
|
|
95
128
|
|
|
96
129
|
## Tool Loop Circuit Breaker
|
|
@@ -606,13 +639,47 @@ puts "#{analysis.robot_name} (#{analysis.duration.round(2)}s): #{analysis.reply}
|
|
|
606
639
|
|
|
607
640
|
---
|
|
608
641
|
|
|
642
|
+
## Live Narration (`RobotLab::Narrator`)
|
|
643
|
+
|
|
644
|
+
`RobotLab::Narrator` is an opt-in [Hook](hooks.md) that narrates what a robot is doing as it happens, to `$stderr` (or any `IO`), so a run is never silent between events. It complements `RobotLab::Audit` (the `robot_lab-audit` gem, which records a persistent history for post-mortem analysis) with a live, human-facing console feed — the two are independent hooks and can both be registered at once.
|
|
645
|
+
|
|
646
|
+
Enable it globally (applies to every robot run, including networks):
|
|
647
|
+
|
|
648
|
+
```ruby
|
|
649
|
+
RobotLab::Narrator.enable! # narrate to $stderr
|
|
650
|
+
RobotLab::Narrator.enable!(output: $stdout) # or any IO
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
Or register it like any hook for a narrower scope:
|
|
654
|
+
|
|
655
|
+
```ruby
|
|
656
|
+
robot.on(RobotLab::Narrator)
|
|
657
|
+
network.on(RobotLab::Narrator)
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
Once registered, a run prints a line per event:
|
|
661
|
+
|
|
662
|
+
```
|
|
663
|
+
· math_bot: thinking…
|
|
664
|
+
· → calculate operation="add"
|
|
665
|
+
· math_bot: thinking…
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
- Before each LLM call: `"<robot name>: thinking…"`
|
|
669
|
+
- Before each tool call: `"→ <tool name> <first arg>=<value>"` — only the *first* argument is shown, truncated to 80 characters
|
|
670
|
+
- After a tool call: `" ✗ <error message>"` — printed only when the tool raised; silent on success
|
|
671
|
+
|
|
672
|
+
Narrator uses `IO#puts` rather than `Kernel#warn`, since `warn` is silenced whenever Ruby warnings are disabled (`$VERBOSE` is `nil`, the common case under `bundle exec`). All three hooks rescue internally, so a narration failure never breaks the underlying run.
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
609
676
|
## See Also
|
|
610
677
|
|
|
611
|
-
- [Robot API](../api/core/robot.md#token
|
|
612
|
-
- [
|
|
613
|
-
- [
|
|
614
|
-
- [
|
|
615
|
-
- [
|
|
616
|
-
- [
|
|
617
|
-
- [
|
|
678
|
+
- [Robot API](../api/core/robot.md#token-cost-tracking)
|
|
679
|
+
- [examples/19_token_tracking.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/19_token_tracking.rb) — Token & Cost Tracking
|
|
680
|
+
- [examples/20_circuit_breaker.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/20_circuit_breaker.rb) — Tool Loop Circuit Breaker
|
|
681
|
+
- [examples/21_learning_loop.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/21_learning_loop.rb) — Learning Accumulation Loop
|
|
682
|
+
- [examples/22_context_compression.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/22_context_compression.rb) — Context Window Compression
|
|
683
|
+
- [examples/23_convergence.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/23_convergence.rb) — Convergence Detection
|
|
684
|
+
- [examples/24_structured_delegation.rb](https://github.com/MadBomber/robot_lab/blob/main/examples/24_structured_delegation.rb) — Structured Delegation
|
|
618
685
|
- [RunConfig reference](../getting-started/configuration.md#runconfig-shared-operational-defaults)
|
data/docs/guides/using-tools.md
CHANGED
|
@@ -133,6 +133,15 @@ robot = RobotLab.build(
|
|
|
133
133
|
)
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
`local_tools:` and `tools:` are different mechanisms — don't confuse them. `local_tools:` attaches tool **instances or classes**; `tools:` (see [Runtime Tool Filtering](#runtime-tool-filtering) below) is a **name allowlist** that filters which of the already-attached tools are sent for a given turn. Passing an instance or class to `tools:` raises `ArgumentError` immediately, naming the offending class and pointing you at `local_tools:` instead:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
RobotLab.build(name: "bot", tools: [GetWeather.new])
|
|
140
|
+
# => ArgumentError: `tools:` expects tool names (String/Symbol) to allow, but
|
|
141
|
+
# received GetWeather. To attach tool instances or classes, pass them as
|
|
142
|
+
# `local_tools:` (e.g. RobotLab.build(local_tools: [MyTool.new])).
|
|
143
|
+
```
|
|
144
|
+
|
|
136
145
|
### Via Template Front Matter
|
|
137
146
|
|
|
138
147
|
Declare tool class names in the template's YAML front matter. RobotLab resolves each string to a Ruby constant via `Object.const_get` and instantiates it:
|
|
@@ -163,6 +172,66 @@ robot = RobotLab.build(name: "assistant", system_prompt: "...")
|
|
|
163
172
|
robot.with_tools(GetWeather, CalculatorTool)
|
|
164
173
|
```
|
|
165
174
|
|
|
175
|
+
## Runtime Tool Filtering
|
|
176
|
+
|
|
177
|
+
`tools:` (and `mcp:`) also work as a **per-run** override, passed to `run()` itself, on top of the build-time/network/global hierarchy described in [Hierarchical MCP and Tools](../getting-started/configuration.md#hierarchical-mcp-and-tools):
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
robot.run("What's the weather?", tools: :inherit) # every attached tool
|
|
181
|
+
robot.run("Just chat, no tools needed.", tools: :none) # zero tools this turn
|
|
182
|
+
robot.run("Only use the calculator.", tools: %w[calculator]) # allowlist by name
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
| Value | Meaning |
|
|
186
|
+
|-------|---------|
|
|
187
|
+
| `:inherit` | No filter — use every tool attached at the robot/network/global level |
|
|
188
|
+
| `:none`, `[]`, `nil` | Send **zero** tools this turn |
|
|
189
|
+
| `["name", ...]` | Only these tool names, by exact match |
|
|
190
|
+
|
|
191
|
+
An explicit `:none`/`[]` is useful for a relevance filter that decided no tool is useful for the current message — it now genuinely sends zero tools for that turn (previously an empty allowlist was silently treated as "all tools," which could overflow small-context local models with the full tool set). Each turn's resolved tool set fully **replaces** the chat's tools rather than accumulating, so a later `:none` turn correctly clears whatever a prior turn attached.
|
|
192
|
+
|
|
193
|
+
> **Watch the default.** Both `Robot.new`'s and `run()`'s `tools:`/`mcp:` parameters default to `:none`, not `:inherit`. If you build a robot with `local_tools:` and then call `robot.run(message)` with no `tools:` override at all, the current runtime default takes the explicit-`:none` path above — sending no tools for that turn. Pass `tools: :inherit` explicitly (at build time, per network task, or per `run()` call — wherever fits your call site) anywhere you need the robot's attached tools available.
|
|
194
|
+
|
|
195
|
+
### Tool Capping and Per-Turn Filtering
|
|
196
|
+
|
|
197
|
+
Most LLM providers reject a tool array longer than 128 entries and fail the whole turn. RobotLab clamps the fully-resolved tool list to a ceiling right before handing it to the chat provider — the definitive choke point regardless of how the tools were configured, filtered, or MCP-connected:
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
robot = RobotLab.build(
|
|
201
|
+
name: "power_user",
|
|
202
|
+
system_prompt: "...",
|
|
203
|
+
local_tools: many_tools, # say, 150 tools
|
|
204
|
+
config: RobotLab::RunConfig.new(max_tools: 50) # override the default ceiling
|
|
205
|
+
)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
- Default ceiling: **128** tools per turn (`RobotLab::Robot::DEFAULT_MAX_TOOLS`)
|
|
209
|
+
- Override with `max_tools:` on `RunConfig` (or the `max_tools:` cascade field — see [Available Fields](../getting-started/configuration.md#available-fields)); `nil` or `<= 0` disables the cap
|
|
210
|
+
- When a turn's resolved tools exceed the cap, RobotLab logs a warning naming how many were dropped and sends the first `max_tools` entries
|
|
211
|
+
|
|
212
|
+
## Skill Scripts and Sandboxing
|
|
213
|
+
|
|
214
|
+
A skill bundle (a directory with a `SKILL.md` plus `scripts/`, discovered via `AgentSkill`) can expose its scripts as tools (`ScriptTool`). Because those scripts run as real OS processes, each `SKILL.md` can declare the capabilities its scripts need directly in front matter, alongside `name`/`description`:
|
|
215
|
+
|
|
216
|
+
```markdown title="skills/deploy-checker/SKILL.md"
|
|
217
|
+
---
|
|
218
|
+
name: deploy-checker
|
|
219
|
+
description: Verifies a deployment's health before promoting it.
|
|
220
|
+
fs_read: ["./data", "/etc/hosts"]
|
|
221
|
+
fs_write: ["./out"]
|
|
222
|
+
network: true
|
|
223
|
+
timeout: 30
|
|
224
|
+
trust: external # or "core" for trusted, always-unconfined skills
|
|
225
|
+
---
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Sandboxing itself is **opt-in and off by default** — see the [`sandbox:` config section](../getting-started/configuration.md#skill-script-sandboxing-sandbox-section). When disabled, scripts run exactly as they always have, unconfined. When enabled:
|
|
229
|
+
|
|
230
|
+
- The global `sandbox:` config is a **ceiling** (`fs_read`, `fs_write`, `network`, `timeout`); each skill's front matter is its **declared** request. The script actually runs under the **intersection** of the two — a path outside the ceiling's roots is dropped even if the skill declares it, `network` requires both sides to allow it, and `timeout` is the smaller of the two.
|
|
231
|
+
- On macOS, confinement is enforced with a generated `sandbox-exec` (Seatbelt) profile: deny-by-default, with narrow allowances for the interpreter to boot, the granted read/write paths, and (optionally) the network. Notably, `$HOME` is never implicitly readable — SSH keys and cloud credentials stay out of reach unless a path under `$HOME` is explicitly granted.
|
|
232
|
+
- Off macOS, or for any skill declaring `trust: core`, sandboxing is a passthrough — confinement is currently macOS-only and is always skipped for trusted "core" skills regardless of platform.
|
|
233
|
+
- A script that runs past its `timeout` is killed (its whole process group) and reported back to the LLM as a timed-out error rather than hanging the turn.
|
|
234
|
+
|
|
166
235
|
## Parameter Types
|
|
167
236
|
|
|
168
237
|
Define parameters on `RubyLLM::Tool` subclasses using `param`:
|
data/docs/index.md
CHANGED
|
@@ -50,7 +50,7 @@ Each robot is backed by a persistent LLM chat, configured with keyword arguments
|
|
|
50
50
|
|
|
51
51
|
Mix reusable prompt behaviors into any robot. Skills expand depth-first with automatic cycle detection and config cascading.
|
|
52
52
|
|
|
53
|
-
[:octicons-arrow-right-24: Skills Guide](guides/building-robots.md#skills)
|
|
53
|
+
[:octicons-arrow-right-24: Skills Guide](guides/building-robots.md#composable-skills)
|
|
54
54
|
|
|
55
55
|
- :material-tools:{ .lg .middle } **Extensible Tools**
|
|
56
56
|
|
data/examples/03_network.rb
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
|
|
12
12
|
require_relative "common"
|
|
13
13
|
|
|
14
|
+
SPECIALIST_TASKS = %i[billing technical general].freeze
|
|
15
|
+
|
|
14
16
|
# Classifier robot that activates the appropriate specialist
|
|
15
17
|
class ClassifierRobot < RobotLab::Robot
|
|
16
18
|
def call(result)
|
|
@@ -76,8 +78,13 @@ network = RobotLab.create_network(name: "support_network", config: shared_config
|
|
|
76
78
|
end
|
|
77
79
|
|
|
78
80
|
banner "Multi-Robot Network"
|
|
79
|
-
puts "
|
|
80
|
-
puts
|
|
81
|
+
puts "Routing plan:"
|
|
82
|
+
puts " Entry task: classifier"
|
|
83
|
+
puts " Optional specialists: #{SPECIALIST_TASKS.join(', ')}"
|
|
84
|
+
puts " Runtime behavior: classifier activates exactly one specialist"
|
|
85
|
+
puts
|
|
86
|
+
puts "Underlying SimpleFlow structure:"
|
|
87
|
+
puts " Optional tasks: #{network.to_h[:optional_tasks].join(', ')}"
|
|
81
88
|
hr
|
|
82
89
|
|
|
83
90
|
# Run the network with a billing question
|
|
@@ -85,6 +92,8 @@ result = network.run(message: "I was charged twice for my subscription last mont
|
|
|
85
92
|
|
|
86
93
|
# Display results
|
|
87
94
|
puts "Network: #{network.name}"
|
|
95
|
+
puts "Executed tasks: #{result.context.keys.grep_v(:run_params).join(', ')}"
|
|
96
|
+
puts "Skipped specialists: #{(SPECIALIST_TASKS - result.context.keys).join(', ')}"
|
|
88
97
|
puts "\nConversation flow:"
|
|
89
98
|
|
|
90
99
|
# Show classifier result
|
data/examples/34_agentskills.rb
CHANGED
|
File without changes
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Example 35: Hooks Architecture with robot_lab-xyzzy
|
|
5
|
+
#
|
|
6
|
+
# Demonstrates the hook handler class pattern. Hooks are named classes that
|
|
7
|
+
# inherit from RobotLab::Hook and implement class methods for each lifecycle
|
|
8
|
+
# phase they care about. All hooks are Ractor-safe by default.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# bundle exec ruby examples/35_hooks.rb
|
|
12
|
+
|
|
13
|
+
require_relative "common"
|
|
14
|
+
require_relative "xyzzy"
|
|
15
|
+
|
|
16
|
+
XYZZY_LOG = File.join(__dir__, "xyzzy_hooks.log")
|
|
17
|
+
RobotLab::Xyzzy.logger = Logger.new(XYZZY_LOG)
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Handler classes used in this demo
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
class RuntimeDemoHook < RobotLab::Hook
|
|
24
|
+
self.namespace = :runtime_demo
|
|
25
|
+
|
|
26
|
+
def self.after_run(ctx)
|
|
27
|
+
ctx.local.reply_seen = ctx.response.reply
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class LocalDemoHook < RobotLab::Hook
|
|
32
|
+
self.namespace = :local_demo
|
|
33
|
+
|
|
34
|
+
def self.before_run(ctx)
|
|
35
|
+
ctx.local.note = "local robot hook state"
|
|
36
|
+
ctx.local.demo = "robot_run"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
class PerfHook < RobotLab::Hook
|
|
41
|
+
self.namespace = :perf
|
|
42
|
+
@timings = []
|
|
43
|
+
@mutex = Mutex.new
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
def reset!
|
|
47
|
+
@mutex.synchronize { @timings = [] }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def timings
|
|
51
|
+
@mutex.synchronize { @timings.dup }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def around_run(ctx, &block)
|
|
55
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
56
|
+
result = block.call
|
|
57
|
+
elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1_000).round(3)
|
|
58
|
+
@mutex.synchronize { @timings << elapsed }
|
|
59
|
+
puts " [perf] #{ctx.request.inspect} — #{elapsed}ms"
|
|
60
|
+
result
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
class GenerationTracerHook < RobotLab::Hook
|
|
66
|
+
self.namespace = :tracer
|
|
67
|
+
|
|
68
|
+
def self.before_llm_generation(ctx)
|
|
69
|
+
puts " [tracer] llm_generation start request=#{ctx.request.inspect}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def self.after_llm_generation(ctx)
|
|
73
|
+
msg = ctx.generation_response
|
|
74
|
+
puts " [tracer] llm_generation done model=#{msg&.model_id} tokens=#{msg&.output_tokens}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class GenCacheHook < RobotLab::Hook
|
|
79
|
+
self.namespace = :gen_cache
|
|
80
|
+
@cache = {}
|
|
81
|
+
@mutex = Mutex.new
|
|
82
|
+
@call_count = 0
|
|
83
|
+
|
|
84
|
+
class << self
|
|
85
|
+
def reset!
|
|
86
|
+
@mutex.synchronize do
|
|
87
|
+
@cache = {}
|
|
88
|
+
@call_count = 0
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def call_count
|
|
93
|
+
@mutex.synchronize { @call_count }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def around_llm_generation(ctx, &block)
|
|
97
|
+
cached = @mutex.synchronize { @cache[ctx.request] }
|
|
98
|
+
if cached
|
|
99
|
+
puts " [gen_cache] HIT — skipping LLM"
|
|
100
|
+
cached
|
|
101
|
+
else
|
|
102
|
+
puts " [gen_cache] MISS — calling LLM"
|
|
103
|
+
@mutex.synchronize { @call_count += 1 }
|
|
104
|
+
block.call.tap { |r| @mutex.synchronize { @cache[ctx.request] = r } }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
class NetworkDemoHook < RobotLab::Hook
|
|
111
|
+
self.namespace = :network_demo
|
|
112
|
+
|
|
113
|
+
def self.before_task(ctx)
|
|
114
|
+
ctx.local.task_note = "network-scoped task hook"
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
HookDemoResponse = Data.define(:content, :tool_calls, :stop_reason) do
|
|
121
|
+
def initialize(content:, tool_calls: nil, stop_reason: "end_turn")
|
|
122
|
+
super
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
class HookDemoTool < RobotLab::Tool
|
|
127
|
+
description "Returns a deterministic hook demo value"
|
|
128
|
+
param :label, type: "string", desc: "The label to echo"
|
|
129
|
+
|
|
130
|
+
def execute(label:)
|
|
131
|
+
{ label: label, status: "handled by HookDemoTool" }
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
class HookDemo
|
|
136
|
+
def run
|
|
137
|
+
banner "Hooks Architecture with robot_lab-xyzzy"
|
|
138
|
+
|
|
139
|
+
explain_setup
|
|
140
|
+
run_robot
|
|
141
|
+
run_llm_loop
|
|
142
|
+
run_tool
|
|
143
|
+
run_network
|
|
144
|
+
run_error
|
|
145
|
+
|
|
146
|
+
hr
|
|
147
|
+
puts "Hook demo complete."
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
def explain_setup
|
|
153
|
+
section "Extension Registration"
|
|
154
|
+
puts <<~TEXT
|
|
155
|
+
xyzzy.rb is a single-file hook extension loaded from examples/.
|
|
156
|
+
|
|
157
|
+
It is a RobotLab::Hook subclass registered under namespace :#{RobotLab::Xyzzy.namespace}.
|
|
158
|
+
stdout : [xyzzy] timestamp hook_name (one line per call)
|
|
159
|
+
logfile : full context snapshot → #{XYZZY_LOG}
|
|
160
|
+
TEXT
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def run_robot
|
|
164
|
+
section "Robot Run Hooks"
|
|
165
|
+
robot = hooked_robot("hook_demo_robot")
|
|
166
|
+
robot.on(LocalDemoHook)
|
|
167
|
+
|
|
168
|
+
result = robot.run("show the hook pipeline", hooks: RuntimeDemoHook)
|
|
169
|
+
|
|
170
|
+
puts "Robot reply: #{result.reply}"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def run_llm_loop
|
|
174
|
+
section "LLM Request/Response Loop"
|
|
175
|
+
puts <<~TEXT
|
|
176
|
+
Demonstrates before/around/after_llm_generation hooks.
|
|
177
|
+
GenCacheHook implements around_llm_generation: on a cache hit the block
|
|
178
|
+
(the real LLM call) is never invoked. PerfHook times each robot.run call.
|
|
179
|
+
TEXT
|
|
180
|
+
|
|
181
|
+
GenCacheHook.reset!
|
|
182
|
+
PerfHook.reset!
|
|
183
|
+
|
|
184
|
+
puts " provider=#{LLM[:default].provider} model=#{LLM[:default].model}\n\n"
|
|
185
|
+
|
|
186
|
+
robot = RobotLab.build(
|
|
187
|
+
name: "loop_demo_robot",
|
|
188
|
+
system_prompt: "You are a helpful assistant. Answer every question in one concise sentence."
|
|
189
|
+
).with_model(LLM[:default].model)
|
|
190
|
+
|
|
191
|
+
robot.on(PerfHook)
|
|
192
|
+
robot.on(GenerationTracerHook)
|
|
193
|
+
robot.on(GenCacheHook)
|
|
194
|
+
|
|
195
|
+
queries = [
|
|
196
|
+
"what is the boiling point of water?",
|
|
197
|
+
"what is 6 * 7?",
|
|
198
|
+
"what is the boiling point of water?",
|
|
199
|
+
"what is 6 * 7?"
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
queries.each_with_index do |query, i|
|
|
203
|
+
puts "\n--- query #{i + 1}: #{query.inspect} ---"
|
|
204
|
+
result = robot.run(query)
|
|
205
|
+
puts " reply: #{result.reply}"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
timings = PerfHook.timings
|
|
209
|
+
cache_hits = queries.size - GenCacheHook.call_count
|
|
210
|
+
avg_ms = (timings.sum / timings.size).round(3)
|
|
211
|
+
puts "\n--- summary ---"
|
|
212
|
+
puts " robot.run calls : #{queries.size}"
|
|
213
|
+
puts " LLM calls made : #{GenCacheHook.call_count} (#{cache_hits} served from gen_cache)"
|
|
214
|
+
puts " avg run time : #{avg_ms}ms"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def run_tool
|
|
218
|
+
section "Tool Call Hooks"
|
|
219
|
+
tool = HookDemoTool.new
|
|
220
|
+
result = tool.call({ "label" => "tool hook payload" })
|
|
221
|
+
puts "Tool result: #{result.inspect}"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def run_network
|
|
225
|
+
section "Network and Task Hooks"
|
|
226
|
+
network = RobotLab::Network.new(name: "hook_demo_network")
|
|
227
|
+
network.on(NetworkDemoHook)
|
|
228
|
+
network.task(:summarize, hooked_robot("network_worker"), depends_on: :none)
|
|
229
|
+
|
|
230
|
+
result = network.run(message: "summarize hook activity")
|
|
231
|
+
reply = result.respond_to?(:reply) ? result.reply : result&.value&.reply
|
|
232
|
+
puts "Network result: #{reply}"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def run_error
|
|
236
|
+
section "Error Hook"
|
|
237
|
+
failing_robot = RobotLab.build(name: "failing_hook_robot", system_prompt: "raise")
|
|
238
|
+
failing_robot.instance_variable_get(:@chat).define_singleton_method(:ask) do |_message = nil, **_kwargs|
|
|
239
|
+
raise "planned hook demo failure"
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
failing_robot.run("trigger the on_error hook")
|
|
243
|
+
rescue RuntimeError => e
|
|
244
|
+
puts "Caught expected error: #{e.message}"
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def hooked_robot(name)
|
|
248
|
+
RobotLab.build(name: name, system_prompt: "deterministic hook demo").tap do |robot|
|
|
249
|
+
robot.instance_variable_get(:@chat).define_singleton_method(:ask) do |message = nil, **_kwargs, &_block|
|
|
250
|
+
HookDemoResponse.new(content: "deterministic response to #{message.inspect}")
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
HookDemo.new.run
|
data/examples/README.md
CHANGED
|
@@ -58,6 +58,7 @@ examples/
|
|
|
58
58
|
29_ractor_tools.rb # Ractor-safe tools: worker pool, freeze_deep, parallel batch
|
|
59
59
|
30_ractor_network.rb # Ractor network scheduler: dependency waves, parallel_mode
|
|
60
60
|
31_launch_assessment.rb # 6 parallel analysts, max_concurrent_robots: 4 semaphore cap
|
|
61
|
+
35_hooks.rb # Hook architecture demo using robot_lab-xyzzy
|
|
61
62
|
18_rails/ # Minimal Rails 8 demo app (full integration)
|
|
62
63
|
app/robots/chat_robot.rb # Robot factory with system prompt + TimeTool
|
|
63
64
|
app/tools/time_tool.rb # Custom RobotLab::Tool subclass
|
|
@@ -306,6 +307,12 @@ Demonstrates: `max_concurrent_robots:` on `RunConfig`, `Async::Semaphore` back-p
|
|
|
306
307
|
|
|
307
308
|
**Requires:** LLM API key
|
|
308
309
|
|
|
310
|
+
### 35 — Hooks Architecture
|
|
311
|
+
|
|
312
|
+
Loads the local `robot_lab-xyzzy` extension, which registers for every hook and logs each callback with the context it receives. Demonstrates robot run, LLM generation, tool call, network run, task, and error hooks with deterministic stubbed responses.
|
|
313
|
+
|
|
314
|
+
**Requires:** None (no LLM calls)
|
|
315
|
+
|
|
309
316
|
### 18 — Rails Integration Demo
|
|
310
317
|
|
|
311
318
|
A minimal, hand-built Rails 8 app that exercises every piece of RobotLab's Rails integration end-to-end. No `rails new` — every file is hand-crafted for minimum size.
|
data/examples/common.rb
CHANGED
|
@@ -10,14 +10,18 @@ require_relative "../lib/robot_lab"
|
|
|
10
10
|
LlmConfig = Data.define(:provider, :model)
|
|
11
11
|
|
|
12
12
|
LLM = {
|
|
13
|
-
default: LlmConfig.new(provider: "openai", model: "gpt-
|
|
13
|
+
default: LlmConfig.new(provider: "openai", model: "gpt-4.1-mini"),
|
|
14
14
|
local: LlmConfig.new(provider: "ollama", model: "llama3.2"),
|
|
15
15
|
anthropic: LlmConfig.new(provider: "anthropic", model: "claude-opus-4-7")
|
|
16
16
|
}.freeze
|
|
17
17
|
|
|
18
18
|
RubyLLM.configure do |c|
|
|
19
|
-
c.logger
|
|
20
|
-
c.default_model
|
|
19
|
+
c.logger = Logger.new(File::NULL)
|
|
20
|
+
c.default_model = LLM[:default].model
|
|
21
|
+
c.openai_api_key = ENV['OPENAI_API_KEY']
|
|
22
|
+
c.openai_organization_id = ENV['OPENAI_ORGANIZATION_ID']
|
|
23
|
+
c.openai_project_id = ENV['OPENAI_PROJECT_ID']
|
|
24
|
+
c.anthropic_api_key = ENV['ANTHROPIC_API_KEY']
|
|
21
25
|
end
|
|
22
26
|
|
|
23
27
|
RobotLab.configure do |c|
|
data/examples/xyzzy.rb
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# xyzzy.rb — single-file RobotLab hook extension demo
|
|
4
|
+
#
|
|
5
|
+
# Demonstrates the Hook handler class pattern: implement class methods for
|
|
6
|
+
# every hook under a dedicated namespace and log each callback with its
|
|
7
|
+
# context snapshot. Useful as a live trace of the full hook pipeline during
|
|
8
|
+
# development.
|
|
9
|
+
#
|
|
10
|
+
# Fully Ractor-safe: no class-level mutable state. LOG_PATH is a frozen
|
|
11
|
+
# string constant (shareable); each call opens its own file handle.
|
|
12
|
+
#
|
|
13
|
+
# stdout : one tagline per hook call → [xyzzy] HH:MM:SS.mmm hook_name
|
|
14
|
+
# logfile: full context snapshot written to LOG_PATH via PP.pp
|
|
15
|
+
#
|
|
16
|
+
# Usage (from any example that requires common):
|
|
17
|
+
# require_relative "xyzzy"
|
|
18
|
+
|
|
19
|
+
require "pp"
|
|
20
|
+
require "fileutils"
|
|
21
|
+
|
|
22
|
+
module RobotLab
|
|
23
|
+
class Xyzzy < Hook
|
|
24
|
+
self.namespace = :xyzzy
|
|
25
|
+
|
|
26
|
+
LOG_PATH = File.expand_path("~/.robot_lab/xyzzy_hooks.log").freeze
|
|
27
|
+
FileUtils.mkdir_p(File.dirname(LOG_PATH))
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
def before_run(ctx) = log_hook(:before_run, ctx)
|
|
31
|
+
def after_run(ctx) = log_hook(:after_run, ctx)
|
|
32
|
+
def on_error(ctx) = log_hook(:on_error, ctx)
|
|
33
|
+
def before_llm_generation(ctx) = log_hook(:before_llm_generation, ctx)
|
|
34
|
+
def after_llm_generation(ctx) = log_hook(:after_llm_generation, ctx)
|
|
35
|
+
def before_tool_call(ctx) = log_hook(:before_tool_call, ctx)
|
|
36
|
+
def after_tool_call(ctx) = log_hook(:after_tool_call, ctx)
|
|
37
|
+
def before_network_run(ctx) = log_hook(:before_network_run, ctx)
|
|
38
|
+
def after_network_run(ctx) = log_hook(:after_network_run, ctx)
|
|
39
|
+
def before_task(ctx) = log_hook(:before_task, ctx)
|
|
40
|
+
def after_task(ctx) = log_hook(:after_task, ctx)
|
|
41
|
+
|
|
42
|
+
def around_run(ctx, &block)
|
|
43
|
+
log_hook(:around_run, ctx)
|
|
44
|
+
block.call
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def around_llm_generation(ctx, &block)
|
|
48
|
+
log_hook(:around_llm_generation, ctx)
|
|
49
|
+
block.call
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def around_tool_call(ctx, &block)
|
|
53
|
+
log_hook(:around_tool_call, ctx)
|
|
54
|
+
block.call
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def around_network_run(ctx, &block)
|
|
58
|
+
log_hook(:around_network_run, ctx)
|
|
59
|
+
block.call
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def around_task(ctx, &block)
|
|
63
|
+
log_hook(:around_task, ctx)
|
|
64
|
+
block.call
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def log_hook(hook_name, ctx)
|
|
70
|
+
ts = Time.now.strftime('%H:%M:%S.%3N')
|
|
71
|
+
snapshot = context_snapshot(ctx)
|
|
72
|
+
$stdout.puts " [xyzzy] #{ts} #{hook_name}"
|
|
73
|
+
File.open(LOG_PATH, "a") { |f| f.puts "#{ts} #{hook_name} | #{PP.pp(snapshot, +"").chomp}" }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def context_snapshot(ctx)
|
|
77
|
+
{
|
|
78
|
+
robot: ctx.respond_to?(:robot) ? ctx.robot&.name : nil,
|
|
79
|
+
request: ctx.respond_to?(:request) ? ctx.request : nil,
|
|
80
|
+
network: ctx.respond_to?(:network) ? ctx.network&.name : nil,
|
|
81
|
+
task: ctx.respond_to?(:task) ? ctx.task : nil,
|
|
82
|
+
error: ctx.respond_to?(:error) ? ctx.error&.message : nil
|
|
83
|
+
}.compact
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
RobotLab.register_extension(:xyzzy, RobotLab::Xyzzy) if RobotLab.respond_to?(:register_extension)
|
|
90
|
+
RobotLab.on(RobotLab::Xyzzy)
|