robot_lab 0.2.6 → 0.2.8
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 +4 -0
- data/.loki +5 -0
- data/Archspec.rb +44 -0
- data/CHANGELOG.md +17 -1
- data/README.md +124 -64
- data/Rakefile +6 -111
- data/_typos.toml +21 -0
- data/docs/api/core/index.md +41 -15
- data/docs/api/core/memory.md +247 -29
- data/docs/api/core/network.md +255 -33
- data/docs/api/core/result.md +120 -32
- data/docs/api/core/robot.md +551 -61
- data/docs/api/core/state.md +87 -197
- data/docs/api/core/tool.md +165 -20
- data/docs/api/errors.md +110 -17
- data/docs/api/hooks.md +469 -0
- data/docs/api/index.md +80 -7
- data/docs/api/mcp/client.md +129 -35
- data/docs/api/mcp/index.md +164 -23
- data/docs/api/mcp/server.md +27 -3
- data/docs/api/mcp/transports.md +94 -22
- data/docs/api/messages/index.md +26 -3
- data/docs/api/messages/text-message.md +33 -11
- data/docs/api/messages/tool-call-message.md +27 -4
- data/docs/api/messages/tool-result-message.md +23 -4
- data/docs/api/messages/user-message.md +45 -8
- data/docs/api/skills.md +423 -0
- data/docs/api/streaming/context.md +28 -5
- data/docs/api/streaming/index.md +57 -11
- data/docs/api/support.md +846 -0
- data/docs/architecture/core-concepts.md +79 -31
- data/docs/architecture/index.md +87 -11
- data/docs/architecture/message-flow.md +66 -29
- data/docs/architecture/network-orchestration.md +145 -38
- data/docs/architecture/robot-execution.md +172 -90
- data/docs/architecture/state-management.md +31 -12
- data/docs/concepts.md +176 -21
- data/docs/examples/basic-chat.md +72 -19
- data/docs/examples/index.md +117 -31
- data/docs/examples/mcp-server.md +154 -45
- data/docs/examples/multi-robot-network.md +91 -21
- data/docs/examples/tool-usage.md +104 -37
- data/docs/getting-started/configuration.md +288 -98
- data/docs/getting-started/installation.md +53 -41
- data/docs/getting-started/quick-start.md +51 -6
- data/docs/guides/building-robots.md +258 -50
- data/docs/guides/creating-networks.md +214 -30
- data/docs/guides/hooks.md +263 -54
- data/docs/guides/knowledge.md +35 -4
- data/docs/guides/mcp-integration.md +211 -44
- data/docs/guides/memory.md +103 -12
- data/docs/guides/observability.md +95 -47
- data/docs/guides/streaming.md +184 -125
- data/docs/guides/using-tools.md +247 -18
- data/docs/index.md +36 -4
- data/examples/01_simple_robot.rb +2 -2
- data/examples/02_tools.rb +14 -4
- data/examples/03_network.rb +12 -7
- data/examples/04_mcp.rb +11 -4
- data/examples/05_streaming.rb +8 -5
- data/examples/06_prompt_templates.rb +13 -9
- data/examples/07_network_memory.rb +5 -5
- data/examples/08_llm_config.rb +20 -15
- data/examples/09_chaining.rb +4 -4
- data/examples/11_network_introspection.rb +4 -4
- data/examples/12_message_bus.rb +2 -2
- data/examples/13_spawn.rb +2 -2
- data/examples/14_rusty_circuit/README.md +1 -0
- data/examples/14_rusty_circuit/comic.rb +7 -3
- data/examples/14_rusty_circuit/display.rb +14 -0
- data/examples/14_rusty_circuit/heckler.rb +8 -6
- data/examples/14_rusty_circuit/open_mic.rb +17 -6
- data/examples/14_rusty_circuit/scout.rb +17 -10
- data/examples/15_memory_network_and_bus/editorial_pipeline.rb +14 -10
- data/examples/15_memory_network_and_bus/linux_writer.rb +2 -2
- data/examples/15_memory_network_and_bus/os_editor.rb +3 -1
- data/examples/15_memory_network_and_bus/os_writer.rb +4 -1
- data/examples/16_writers_room/writer.rb +22 -22
- data/examples/16_writers_room/writers_room.rb +2 -0
- data/examples/17_skills.rb +14 -13
- data/examples/18_rails/README.md +20 -1
- data/examples/18_rails/app/controllers/chat_controller.rb +5 -1
- data/examples/18_rails/app/jobs/robot_run_job.rb +11 -5
- data/examples/18_rails/app/robots/chat_robot.rb +11 -0
- data/examples/18_rails/config/initializers/robot_lab.rb +8 -0
- data/examples/19_token_tracking.rb +25 -9
- data/examples/20_circuit_breaker.rb +10 -7
- data/examples/21_learning_loop.rb +42 -16
- data/examples/22_context_compression.rb +23 -23
- data/examples/23_convergence.rb +24 -17
- data/examples/24_structured_delegation.rb +13 -8
- data/examples/25_history_search.rb +12 -8
- data/examples/27_incident_response/incident_response.rb +31 -13
- data/examples/28_mcp_discovery.rb +17 -13
- data/examples/29_ractor_tools.rb +4 -2
- data/examples/30_ractor_network.rb +22 -17
- data/examples/31_launch_assessment.rb +20 -9
- data/examples/32_newsletter_reader.rb +7 -2
- data/examples/33_stock_predictor.rb +34 -13
- data/examples/34_agentskills.rb +7 -3
- data/examples/35_hooks.rb +18 -8
- data/examples/README.md +199 -45
- data/examples/common.rb +79 -11
- data/examples/xyzzy.rb +8 -1
- data/lib/robot_lab/agent_skill_catalog.rb +1 -0
- data/lib/robot_lab/ask_user.rb +2 -0
- data/lib/robot_lab/bus_poller.rb +2 -0
- data/lib/robot_lab/capabilities.rb +4 -0
- data/lib/robot_lab/config.rb +18 -5
- data/lib/robot_lab/doom_loop_detector.rb +6 -3
- data/lib/robot_lab/history_compressor.rb +5 -0
- data/lib/robot_lab/hook.rb +1 -0
- data/lib/robot_lab/hook_context.rb +4 -0
- data/lib/robot_lab/hook_registry.rb +1 -0
- data/lib/robot_lab/hooks.rb +6 -3
- data/lib/robot_lab/mcp/client.rb +2 -2
- data/lib/robot_lab/mcp/connection_poller.rb +16 -8
- data/lib/robot_lab/mcp/server_discovery.rb +1 -0
- data/lib/robot_lab/mcp/transports/sse.rb +3 -0
- data/lib/robot_lab/mcp/transports/stdio.rb +5 -0
- data/lib/robot_lab/mcp/transports/streamable_http.rb +4 -0
- data/lib/robot_lab/mcp/transports/websocket.rb +3 -0
- data/lib/robot_lab/memory.rb +23 -6
- data/lib/robot_lab/memory_change.rb +1 -0
- data/lib/robot_lab/message.rb +3 -0
- data/lib/robot_lab/names.rb +400 -0
- data/lib/robot_lab/network.rb +13 -6
- data/lib/robot_lab/robot/agent_skill_matching.rb +3 -3
- data/lib/robot_lab/robot/bus_messaging.rb +19 -8
- data/lib/robot_lab/robot/history_search.rb +2 -0
- data/lib/robot_lab/robot/hooking.rb +3 -0
- data/lib/robot_lab/robot/mcp_management.rb +11 -3
- data/lib/robot_lab/robot/template_rendering.rb +29 -7
- data/lib/robot_lab/robot.rb +81 -19
- data/lib/robot_lab/robot_result.rb +3 -0
- data/lib/robot_lab/run_config.rb +5 -0
- data/lib/robot_lab/script_tool.rb +20 -39
- data/lib/robot_lab/state_proxy.rb +1 -0
- data/lib/robot_lab/streaming/context.rb +1 -0
- data/lib/robot_lab/streaming/events.rb +1 -0
- data/lib/robot_lab/task.rb +2 -0
- data/lib/robot_lab/tool.rb +4 -0
- data/lib/robot_lab/user_message.rb +1 -0
- data/lib/robot_lab/utils.rb +2 -0
- data/lib/robot_lab/version.rb +1 -1
- data/lib/robot_lab/waiter.rb +3 -0
- data/lib/robot_lab.rb +27 -18
- data/mkdocs.yml +6 -1
- metadata +12 -7
- data/lib/robot_lab/sandbox/null.rb +0 -13
- data/lib/robot_lab/sandbox/seatbelt.rb +0 -104
- data/lib/robot_lab/sandbox.rb +0 -52
data/Rakefile
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
# Quality gates (quality, rubocop_check, flog_check, flay_check,
|
|
4
|
+
# archspec_check, ...), doc *serving*, and the gem lifecycle (build, install,
|
|
5
|
+
# release) live in asgard — see .loki and the shared dev/*.loki files it
|
|
6
|
+
# imports. This Rakefile keeps the tasks asgard delegates to (tests,
|
|
7
|
+
# integration, examples, docs:build).
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
require "rake/testtask"
|
|
7
10
|
|
|
8
11
|
Rake::TestTask.new(:test) do |t|
|
|
9
12
|
t.libs << "test"
|
|
@@ -36,100 +39,6 @@ Rake::TestTask.new(:integration) do |t|
|
|
|
36
39
|
t.ruby_opts << "-rtest_helper"
|
|
37
40
|
end
|
|
38
41
|
|
|
39
|
-
desc "Check code complexity with Flog (warn ≥20, fail ≥50)"
|
|
40
|
-
task :flog_check do
|
|
41
|
-
require 'flog'
|
|
42
|
-
|
|
43
|
-
# Target to work toward; methods above this are warned but don't fail the gate.
|
|
44
|
-
METHOD_WARN = 20.0
|
|
45
|
-
# Current baseline floor — established from first run. Reduce incrementally.
|
|
46
|
-
METHOD_FAIL = 50.0
|
|
47
|
-
|
|
48
|
-
flogger = Flog.new(all: true)
|
|
49
|
-
flogger.flog(*Dir.glob('lib/**/*.rb'))
|
|
50
|
-
|
|
51
|
-
warnings = []
|
|
52
|
-
failures = []
|
|
53
|
-
|
|
54
|
-
flogger.each_by_score do |method, score|
|
|
55
|
-
next if method.end_with?('#none') # skip file-level non-method code
|
|
56
|
-
if score > METHOD_FAIL
|
|
57
|
-
failures << "#{'%.1f' % score}: #{method}"
|
|
58
|
-
elsif score > METHOD_WARN
|
|
59
|
-
warnings << "#{'%.1f' % score}: #{method}"
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
unless warnings.empty?
|
|
64
|
-
puts "\nFlog warnings (#{METHOD_WARN}–#{METHOD_FAIL}) — target for future refactoring:"
|
|
65
|
-
warnings.each { |v| puts " #{v}" }
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
if failures.empty?
|
|
69
|
-
puts "\nFlog: no methods exceed the failure threshold (≥#{METHOD_FAIL})"
|
|
70
|
-
else
|
|
71
|
-
puts "\nFlog failures (≥#{METHOD_FAIL}) — must be refactored:"
|
|
72
|
-
failures.each { |v| puts " #{v}" }
|
|
73
|
-
abort "\nFlog quality gate failed: #{failures.size} method(s) exceed #{METHOD_FAIL}"
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
desc "Check for structural code duplication with Flay (mass >= 50)"
|
|
78
|
-
task :flay_check do
|
|
79
|
-
require 'flay'
|
|
80
|
-
|
|
81
|
-
mass_threshold = 50
|
|
82
|
-
|
|
83
|
-
flay = Flay.new({ mass: mass_threshold, diff: false, verbose: false, summary: false, timeout: 60 })
|
|
84
|
-
flay.process(*Dir.glob('lib/**/*.rb'))
|
|
85
|
-
flay.analyze
|
|
86
|
-
|
|
87
|
-
if flay.hashes.empty?
|
|
88
|
-
puts "\nFlay: no structural duplication detected (mass >= #{mass_threshold})"
|
|
89
|
-
else
|
|
90
|
-
puts "\nFlay found structural duplication (mass >= #{mass_threshold}):"
|
|
91
|
-
flay.report
|
|
92
|
-
abort "\nFlay quality gate failed: #{flay.hashes.length} pattern(s) detected"
|
|
93
|
-
end
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
desc "Run all quality checks: tests (with coverage), RuboCop, Flog, and Flay"
|
|
97
|
-
task :quality do
|
|
98
|
-
results = {}
|
|
99
|
-
|
|
100
|
-
puts "\n#{'=' * 60}"
|
|
101
|
-
puts "Quality Gate: Tests + Coverage"
|
|
102
|
-
puts '=' * 60
|
|
103
|
-
results[:tests] = system("bundle exec rake test") ? :pass : :fail
|
|
104
|
-
|
|
105
|
-
puts "\n#{'=' * 60}"
|
|
106
|
-
puts "Quality Gate: RuboCop"
|
|
107
|
-
puts '=' * 60
|
|
108
|
-
results[:rubocop] = system(RUBOCOP_ENV, "bundle exec rubocop") ? :pass : :fail
|
|
109
|
-
|
|
110
|
-
puts "\n#{'=' * 60}"
|
|
111
|
-
puts "Quality Gate: Flog Complexity"
|
|
112
|
-
puts '=' * 60
|
|
113
|
-
results[:flog] = system("bundle exec rake flog_check") ? :pass : :fail
|
|
114
|
-
|
|
115
|
-
puts "\n#{'=' * 60}"
|
|
116
|
-
puts "Quality Gate: Flay Duplication"
|
|
117
|
-
puts '=' * 60
|
|
118
|
-
results[:flay] = system("bundle exec rake flay_check") ? :pass : :fail
|
|
119
|
-
|
|
120
|
-
puts "\n#{'=' * 60}"
|
|
121
|
-
puts "Quality Summary"
|
|
122
|
-
puts '=' * 60
|
|
123
|
-
results.each do |gate, status|
|
|
124
|
-
icon = status == :pass ? 'PASS' : 'FAIL'
|
|
125
|
-
puts " [#{icon}] #{gate}"
|
|
126
|
-
end
|
|
127
|
-
puts '=' * 60
|
|
128
|
-
|
|
129
|
-
abort "\nQuality gate failed" if results.values.any?(:fail)
|
|
130
|
-
puts "\nAll quality gates passed."
|
|
131
|
-
end
|
|
132
|
-
|
|
133
42
|
namespace :examples do
|
|
134
43
|
# Map of subdirectory-based demos to their entry point scripts
|
|
135
44
|
SUBDIR_ENTRY_POINTS = {
|
|
@@ -277,22 +186,8 @@ namespace :docs do
|
|
|
277
186
|
sh "yard doc"
|
|
278
187
|
end
|
|
279
188
|
|
|
280
|
-
namespace :yard do
|
|
281
|
-
desc "Serve YARD documentation locally"
|
|
282
|
-
task :serve do
|
|
283
|
-
sh "yard server --reload"
|
|
284
|
-
end
|
|
285
|
-
end
|
|
286
|
-
|
|
287
189
|
desc "Build MkDocs documentation"
|
|
288
190
|
task :mkdocs do
|
|
289
191
|
sh "mkdocs build"
|
|
290
192
|
end
|
|
291
|
-
|
|
292
|
-
namespace :mkdocs do
|
|
293
|
-
desc "Serve MkDocs documentation locally"
|
|
294
|
-
task :serve do
|
|
295
|
-
sh "mkdocs serve"
|
|
296
|
-
end
|
|
297
|
-
end
|
|
298
193
|
end
|
data/_typos.toml
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# False-positive suppressions for the typos quality gate (asgard).
|
|
2
|
+
# Everything here was reviewed by hand — see git history for context.
|
|
3
|
+
|
|
4
|
+
[files]
|
|
5
|
+
extend-exclude = [
|
|
6
|
+
# Example transcripts / articles: prose quotes real names ("Damon Clark")
|
|
7
|
+
# and SQL-ish plurals ("SELECTs") that typos mis-parses.
|
|
8
|
+
"examples/ruboruby.md",
|
|
9
|
+
"examples/25_history_search/conversation.jsonl",
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
[default.extend-identifiers]
|
|
13
|
+
# Robot names in lib/robot_lab/names.rb
|
|
14
|
+
BATs = "BATs"
|
|
15
|
+
Damon = "Damon"
|
|
16
|
+
Mata_Nui = "Mata_Nui"
|
|
17
|
+
Pris = "Pris"
|
|
18
|
+
S1MONE = "S1MONE"
|
|
19
|
+
# Test-local variable names
|
|
20
|
+
ba = "ba" # b→a similarity (convergence_test.rb)
|
|
21
|
+
thr = "thr" # thread handle (connection_poller_test.rb)
|
data/docs/api/core/index.md
CHANGED
|
@@ -10,16 +10,28 @@ classDiagram
|
|
|
10
10
|
+name: String
|
|
11
11
|
+description: String
|
|
12
12
|
+model: String
|
|
13
|
-
+template:
|
|
14
|
-
+
|
|
15
|
-
+
|
|
13
|
+
+template: Symbol
|
|
14
|
+
+local_tools: Array~Tool~
|
|
15
|
+
+mcp_tools: Array~Tool~
|
|
16
|
+
+tools_config: Symbol_or_Array
|
|
17
|
+
+run(message) RobotResult
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class RobotResult {
|
|
21
|
+
+robot_name: String
|
|
22
|
+
+reply: String
|
|
23
|
+
+output: Array~TextMessage~
|
|
24
|
+
+input_tokens: Integer
|
|
25
|
+
+output_tokens: Integer
|
|
26
|
+
+export() Hash
|
|
16
27
|
}
|
|
17
28
|
|
|
18
29
|
class Network {
|
|
19
30
|
+name: String
|
|
20
31
|
+robots: Hash
|
|
21
32
|
+config: RunConfig
|
|
22
|
-
+
|
|
33
|
+
+memory: Memory
|
|
34
|
+
+run(message) SimpleFlow_Result
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
class RunConfig {
|
|
@@ -32,8 +44,9 @@ classDiagram
|
|
|
32
44
|
class Tool {
|
|
33
45
|
+name: String
|
|
34
46
|
+description: String
|
|
35
|
-
+
|
|
36
|
-
+
|
|
47
|
+
+robot: Robot
|
|
48
|
+
+mcp: String
|
|
49
|
+
+call(args)
|
|
37
50
|
}
|
|
38
51
|
|
|
39
52
|
class Memory {
|
|
@@ -60,22 +73,28 @@ classDiagram
|
|
|
60
73
|
Robot --> RunConfig : uses
|
|
61
74
|
Robot --> Tool : has
|
|
62
75
|
Robot --> Memory : uses
|
|
76
|
+
Robot ..> RobotResult : returns
|
|
63
77
|
Network --> Memory : uses
|
|
64
78
|
Robot ..> RobotMessage : sends/receives
|
|
79
|
+
Memory --> StateProxy : data
|
|
65
80
|
```
|
|
66
81
|
|
|
67
82
|
## Classes
|
|
68
83
|
|
|
69
84
|
| Class | Purpose |
|
|
70
85
|
|-------|---------|
|
|
71
|
-
| [Robot](robot.md) | LLM agent with
|
|
72
|
-
| [
|
|
73
|
-
|
|
|
74
|
-
| [
|
|
86
|
+
| [Robot](robot.md) | LLM agent with templates, tools, memory, and model configuration |
|
|
87
|
+
| [RobotResult](result.md) | Value object returned by `Robot#run` |
|
|
88
|
+
| [Network](network.md) | Container for robots with DAG orchestration |
|
|
89
|
+
| [RunConfig](robot.md#runconfig) | Shared configuration for LLM, tools, callbacks, and infrastructure |
|
|
90
|
+
| [Tool](tool.md) | Callable function with parameters and an `execute` method |
|
|
75
91
|
| [AskUser](tool.md#built-in-askuser) | Built-in tool for terminal-based user interaction |
|
|
76
92
|
| [Memory](memory.md) | Reactive key-value store for sharing data |
|
|
93
|
+
| [StateProxy](state.md) | Hash/method-access wrapper returned by `memory.data` |
|
|
77
94
|
| RobotMessage | Typed envelope for bus-based inter-robot communication |
|
|
78
|
-
| `RobotLab::Runnable` | Shared interface (`crew`, `chief`, `
|
|
95
|
+
| `RobotLab::Runnable` | Shared interface (`crew`, `chief`, `robot_count`, `network?`, `single?`) implemented by both `Robot` and `Network` — see [Runnable Protocol](../../architecture/core-concepts.md#runnable-protocol) |
|
|
96
|
+
|
|
97
|
+
There is no `RobotLab::State` class and no `RobotLab::NetworkRun` class.
|
|
79
98
|
|
|
80
99
|
## Quick Examples
|
|
81
100
|
|
|
@@ -89,18 +108,25 @@ robot = RobotLab.build(
|
|
|
89
108
|
local_tools: [greet_tool]
|
|
90
109
|
)
|
|
91
110
|
|
|
92
|
-
result = robot.run("Hello!")
|
|
111
|
+
result = robot.run("Hello!") # no tools sent — run() defaults to tools: :none
|
|
112
|
+
result = robot.run("Hello!", tools: :inherit) # sends greet_tool
|
|
113
|
+
result.reply # => String
|
|
93
114
|
```
|
|
94
115
|
|
|
116
|
+
`Robot#run` returns a [`RobotLab::RobotResult`](result.md), not a `Message`.
|
|
117
|
+
|
|
95
118
|
### Network
|
|
96
119
|
|
|
120
|
+
The builder DSL method is `task` (there is no `step`):
|
|
121
|
+
|
|
97
122
|
```ruby
|
|
98
123
|
network = RobotLab.create_network(name: "my_network") do
|
|
99
|
-
|
|
100
|
-
|
|
124
|
+
task :analyzer, analyzer_robot, depends_on: :none
|
|
125
|
+
task :writer, writer_robot, depends_on: [:analyzer]
|
|
101
126
|
end
|
|
102
127
|
|
|
103
|
-
result = network.run(message: "Process this")
|
|
128
|
+
result = network.run(message: "Process this") # => SimpleFlow::Result
|
|
129
|
+
result.value # => RobotResult of the last task
|
|
104
130
|
```
|
|
105
131
|
|
|
106
132
|
### Memory
|
data/docs/api/core/memory.md
CHANGED
|
@@ -83,14 +83,33 @@ Retrieve one or more values, optionally waiting until they exist.
|
|
|
83
83
|
|
|
84
84
|
**Parameters:**
|
|
85
85
|
|
|
86
|
-
| Name | Type | Description |
|
|
87
|
-
|
|
88
|
-
|
|
|
89
|
-
| `wait` | `Boolean`, `Numeric` | `false`:
|
|
90
|
-
|
|
91
|
-
**Returns:**
|
|
92
|
-
|
|
93
|
-
**Raises:** `AwaitTimeout`
|
|
86
|
+
| Name | Type | Default | Description |
|
|
87
|
+
|------|------|---------|-------------|
|
|
88
|
+
| `*keys` | `Symbol`, `String` | — | One or more keys to retrieve; flattened and symbolized |
|
|
89
|
+
| `wait` | `Boolean`, `Numeric` | `false` | `false`: return immediately (nil if missing). `true`: block indefinitely. `Numeric`: block up to that many seconds |
|
|
90
|
+
|
|
91
|
+
**Returns:** the single value for one key, a `Hash` keyed by symbol for multiple keys.
|
|
92
|
+
|
|
93
|
+
**Raises:** `RobotLab::AwaitTimeout` — `"Timeout waiting for :<key> after <N> seconds"`.
|
|
94
|
+
|
|
95
|
+
!!! warning "Blocking semantics"
|
|
96
|
+
- On expiry `get` **raises `RobotLab::AwaitTimeout`**; it does not return nil.
|
|
97
|
+
Rescue it if a missing value is acceptable.
|
|
98
|
+
- With multiple keys the timeout is applied **per missing key**, not to the
|
|
99
|
+
call as a whole. `get(:a, :b, :c, wait: 60)` can block for up to 180
|
|
100
|
+
seconds if all three are missing.
|
|
101
|
+
- `wait: true` blocks with no deadline and can hang forever. Prefer a numeric
|
|
102
|
+
timeout in production.
|
|
103
|
+
- Waiting is implemented with a pipe (`Waiter`, using `IO#wait_readable`), so
|
|
104
|
+
a blocked reader does not spin.
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
value = begin
|
|
108
|
+
memory.get(:sentiment, wait: 30)
|
|
109
|
+
rescue RobotLab::AwaitTimeout
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
```
|
|
94
113
|
|
|
95
114
|
### key?
|
|
96
115
|
|
|
@@ -114,7 +133,16 @@ Remove a key. Cannot delete reserved keys.
|
|
|
114
133
|
memory.keys # => Array<Symbol>
|
|
115
134
|
```
|
|
116
135
|
|
|
117
|
-
|
|
136
|
+
All keys **excluding** the reserved ones.
|
|
137
|
+
|
|
138
|
+
### all_keys
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
memory.all_keys # => Array<Symbol>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
All keys **including** the reserved ones (`:data`, `:results`, `:messages`,
|
|
145
|
+
`:session_id`, `:cache`).
|
|
118
146
|
|
|
119
147
|
### clear
|
|
120
148
|
|
|
@@ -130,7 +158,14 @@ Clear all non-reserved keys.
|
|
|
130
158
|
memory.reset
|
|
131
159
|
```
|
|
132
160
|
|
|
133
|
-
Reset memory to initial state
|
|
161
|
+
Reset memory to its initial state: clears the backend, restores `:data` to `{}`,
|
|
162
|
+
`:results` and `:messages` to `[]`, `:session_id` to `nil`, and re-installs the
|
|
163
|
+
existing cache object. The `StateProxy` returned by `data` is discarded and
|
|
164
|
+
rebuilt on next access.
|
|
165
|
+
|
|
166
|
+
This resets the **key-value store only**. It has nothing to do with a robot's
|
|
167
|
+
chat history — use `robot.clear_messages(keep_system: true)` for that. The two
|
|
168
|
+
are independent.
|
|
134
169
|
|
|
135
170
|
### subscribe
|
|
136
171
|
|
|
@@ -141,20 +176,41 @@ end
|
|
|
141
176
|
```
|
|
142
177
|
|
|
143
178
|
Subscribe to changes on one or more keys. Callback receives a `MemoryChange` object.
|
|
179
|
+
Returns a subscription ID for [`unsubscribe`](#unsubscribe).
|
|
180
|
+
|
|
181
|
+
**Raises:** `ArgumentError` if no block is given.
|
|
182
|
+
|
|
183
|
+
Only [`set`](#set) notifies subscribers. Writing a reserved key
|
|
184
|
+
(`memory[:data] = ...`, `session_id=`, `append_result`) bypasses notification
|
|
185
|
+
entirely, as does mutating `memory.data`.
|
|
144
186
|
|
|
145
|
-
**
|
|
187
|
+
Callbacks are dispatched through `Async { }`. **Outside a running reactor the
|
|
188
|
+
callback runs synchronously on the writer's thread**, so a slow subscriber blocks
|
|
189
|
+
the `set` that triggered it.
|
|
190
|
+
|
|
191
|
+
**`RobotLab::MemoryChange` attributes:**
|
|
146
192
|
|
|
147
193
|
| Attribute | Type | Description |
|
|
148
194
|
|-----------|------|-------------|
|
|
149
195
|
| `key` | `Symbol` | The changed key |
|
|
150
196
|
| `value` | `Object` | New value |
|
|
151
|
-
| `previous` | `Object` | Previous value |
|
|
152
|
-
| `writer` | `String, nil` |
|
|
197
|
+
| `previous` | `Object, nil` | Previous value |
|
|
198
|
+
| `writer` | `String, nil` | `memory.current_writer` at the time of the write — set to the robot's name for the duration of each `run` |
|
|
153
199
|
| `network_name` | `String, nil` | Network name |
|
|
154
200
|
| `timestamp` | `Time` | When the change occurred |
|
|
155
|
-
| `
|
|
156
|
-
|
|
157
|
-
|
|
201
|
+
| `correlation_id` | `String, nil` | Optional tracing ID |
|
|
202
|
+
|
|
203
|
+
**Predicates** — note that each tests *both* sides, so all three are false when
|
|
204
|
+
`previous` and `value` are both nil, and all three are false for a nil→nil write:
|
|
205
|
+
|
|
206
|
+
| Predicate | Exact definition |
|
|
207
|
+
|-----------|------------------|
|
|
208
|
+
| `created?` | `previous.nil? && !value.nil?` |
|
|
209
|
+
| `updated?` | `!previous.nil? && !value.nil?` |
|
|
210
|
+
| `deleted?` | `value.nil? && !previous.nil?` |
|
|
211
|
+
|
|
212
|
+
Also available: `to_h` (`.compact`ed, `timestamp` rendered as ISO-8601),
|
|
213
|
+
`to_json`, and `MemoryChange.from_hash`.
|
|
158
214
|
|
|
159
215
|
### subscribe_pattern
|
|
160
216
|
|
|
@@ -164,7 +220,8 @@ sub_id = memory.subscribe_pattern("analysis:*") do |change|
|
|
|
164
220
|
end
|
|
165
221
|
```
|
|
166
222
|
|
|
167
|
-
Subscribe to keys matching a glob pattern (`*` and `?` supported).
|
|
223
|
+
Subscribe to keys matching a glob pattern (`*` and `?` supported). Returns a
|
|
224
|
+
subscription ID. **Raises:** `ArgumentError` if no block is given.
|
|
168
225
|
|
|
169
226
|
### unsubscribe
|
|
170
227
|
|
|
@@ -172,15 +229,131 @@ Subscribe to keys matching a glob pattern (`*` and `?` supported).
|
|
|
172
229
|
memory.unsubscribe(sub_id) # => Boolean
|
|
173
230
|
```
|
|
174
231
|
|
|
175
|
-
Remove a subscription by its ID
|
|
232
|
+
Remove a subscription by its ID (works for both `subscribe` and
|
|
233
|
+
`subscribe_pattern`). Returns `true` when something was removed.
|
|
234
|
+
|
|
235
|
+
### unsubscribe_keys
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
memory.unsubscribe_keys(:status, :progress) # => self
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Drop **all** key subscriptions for the named keys at once, without needing their
|
|
242
|
+
IDs. Pattern subscriptions are unaffected.
|
|
243
|
+
|
|
244
|
+
### subscribed?
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
memory.subscribed?(:status) # => Boolean
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Whether any subscriber — key-based or pattern-based — would be notified for `key`.
|
|
176
251
|
|
|
177
252
|
### merge!
|
|
178
253
|
|
|
179
254
|
```ruby
|
|
180
|
-
memory.merge!(key1: "value1", key2: "value2")
|
|
255
|
+
memory.merge!(key1: "value1", key2: "value2") # => self
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Merge multiple key-value pairs into memory. Each pair is assigned through `[]=`,
|
|
259
|
+
so non-reserved keys go through the reactive `set` path and do notify subscribers.
|
|
260
|
+
|
|
261
|
+
## Results and History
|
|
262
|
+
|
|
263
|
+
### append_result
|
|
264
|
+
|
|
265
|
+
```ruby
|
|
266
|
+
memory.append_result(robot_result) # => self
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Push a `RobotResult` onto the accumulated `:results` array. Bypasses subscriber
|
|
270
|
+
notification.
|
|
271
|
+
|
|
272
|
+
### set_results
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
memory.set_results(array_of_results) # => self
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Replace the whole `:results` array (used when loading from persistence).
|
|
279
|
+
|
|
280
|
+
### results_from
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
memory.results_from(5) # => Array<RobotResult>
|
|
181
284
|
```
|
|
182
285
|
|
|
183
|
-
|
|
286
|
+
Results from the given index onward — for incremental saves. Returns `[]` when
|
|
287
|
+
the index is past the end.
|
|
288
|
+
|
|
289
|
+
### format_history
|
|
290
|
+
|
|
291
|
+
```ruby
|
|
292
|
+
memory.format_history(formatter: nil) # => Array<Message>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
`messages` followed by every result flat-mapped through `formatter`. Pass a
|
|
296
|
+
`Proc` for `formatter:` to control how a `RobotResult` becomes messages; the
|
|
297
|
+
default formatter is used when omitted.
|
|
298
|
+
|
|
299
|
+
## Backend
|
|
300
|
+
|
|
301
|
+
### redis?
|
|
302
|
+
|
|
303
|
+
```ruby
|
|
304
|
+
memory.redis? # => Boolean
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Whether this memory is backed by Redis rather than the in-process Hash. The
|
|
308
|
+
`backend: :auto` default tries Redis and falls back to a Hash; `backend: :hash`
|
|
309
|
+
forces the Hash.
|
|
310
|
+
|
|
311
|
+
### network_name
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
memory.network_name # => String, nil
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The network this memory belongs to, set once at construction and read-only
|
|
318
|
+
thereafter. `Network` creates its shared memory as `Memory.new(network_name: name)`;
|
|
319
|
+
a standalone robot's inherent memory has `nil`. It is copied onto every
|
|
320
|
+
`MemoryChange` so a subscriber can tell which network a write came from, and it is
|
|
321
|
+
preserved by `clone`/`dup` — but **not** by `from_hash`.
|
|
322
|
+
|
|
323
|
+
### current_writer / current_writer=
|
|
324
|
+
|
|
325
|
+
```ruby
|
|
326
|
+
memory.current_writer # => String, nil
|
|
327
|
+
memory.current_writer = "bot"
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
The name attributed to writes, surfaced as `MemoryChange#writer`. `Robot#run`
|
|
331
|
+
sets this to the robot's name for the duration of the run and restores the
|
|
332
|
+
previous value in an `ensure` block, so nested and concurrent runs attribute
|
|
333
|
+
correctly.
|
|
334
|
+
|
|
335
|
+
## Document Store
|
|
336
|
+
|
|
337
|
+
These four methods require the **`robot_lab-document_store`** extension gem. Without
|
|
338
|
+
it every one of them raises
|
|
339
|
+
`RobotLab::DependencyError: document storage requires the robot_lab-document_store gem.`
|
|
340
|
+
|
|
341
|
+
| Method | Returns | Description |
|
|
342
|
+
|--------|---------|-------------|
|
|
343
|
+
| `store_document(key, text)` | `self` | Embed `text` and store it under `key` |
|
|
344
|
+
| `search_documents(query, limit: 5)` | `Array<Hash>` | Hits sorted by score descending; each hash has `:key`, `:text`, `:score` |
|
|
345
|
+
| `document_keys` | `Array<Symbol>` | Keys of all stored documents |
|
|
346
|
+
| `delete_document(key)` | `self` | Remove a document |
|
|
347
|
+
|
|
348
|
+
```ruby
|
|
349
|
+
memory.store_document(:readme, File.read("README.md"))
|
|
350
|
+
memory.search_documents("how to configure redis", limit: 3).each do |hit|
|
|
351
|
+
puts "#{hit[:key]} (#{hit[:score].round(3)})"
|
|
352
|
+
end
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
Documents live in a separate store, not in the key-value backend — they do not
|
|
356
|
+
appear in `keys`, `all_keys`, or `to_h`.
|
|
184
357
|
|
|
185
358
|
## Reserved Key Accessors
|
|
186
359
|
|
|
@@ -193,7 +366,9 @@ memory.data.user_id # Method access
|
|
|
193
366
|
memory.data[:status] = "active"
|
|
194
367
|
```
|
|
195
368
|
|
|
196
|
-
Runtime data accessed through `StateProxy` for method-style access.
|
|
369
|
+
Runtime data accessed through a [`StateProxy`](state.md) for method-style access.
|
|
370
|
+
Writes made through the proxy are **not** reactive — they bypass `set`, so they
|
|
371
|
+
wake no blocking readers and notify no subscribers.
|
|
197
372
|
|
|
198
373
|
### results
|
|
199
374
|
|
|
@@ -221,10 +396,22 @@ memory.session_id = "abc" # Set session identifier
|
|
|
221
396
|
### cache
|
|
222
397
|
|
|
223
398
|
```ruby
|
|
224
|
-
memory.cache # => RubyLLM::SemanticCache
|
|
399
|
+
memory.cache # => RubyLLM::SemanticCache (the module itself), or nil
|
|
225
400
|
```
|
|
226
401
|
|
|
227
|
-
|
|
402
|
+
Read-only after initialization — assigning it raises
|
|
403
|
+
`ArgumentError: Cannot reassign cache - it is initialized automatically`.
|
|
404
|
+
|
|
405
|
+
!!! warning "`cache` is `nil` when caching is disabled"
|
|
406
|
+
The value stored is the `RubyLLM::SemanticCache` **module**, not an instance.
|
|
407
|
+
When constructed with `enable_cache: false` it is `nil`, so
|
|
408
|
+
`memory.cache.fetch(...)` raises `NoMethodError`. Guard on
|
|
409
|
+
`memory.cache` before use, or leave `enable_cache` at its `true` default.
|
|
410
|
+
|
|
411
|
+
```ruby
|
|
412
|
+
RobotLab::Memory.new.cache # => RubyLLM::SemanticCache
|
|
413
|
+
RobotLab::Memory.new(enable_cache: false).cache # => nil
|
|
414
|
+
```
|
|
228
415
|
|
|
229
416
|
## Serialization
|
|
230
417
|
|
|
@@ -235,27 +422,50 @@ memory.to_h
|
|
|
235
422
|
# => { data: {...}, results: [...], messages: [...], session_id: "...", custom: {...} }
|
|
236
423
|
```
|
|
237
424
|
|
|
425
|
+
Keys: `data` (from the `StateProxy`), `results` (each via `RobotResult#export`),
|
|
426
|
+
`messages` (each via `to_h`), `session_id`, and `custom` (every non-reserved key).
|
|
427
|
+
`cache` is never serialized.
|
|
428
|
+
|
|
429
|
+
!!! warning "`to_h` is `.compact`ed"
|
|
430
|
+
Nil values are dropped, so `session_id` disappears entirely when unset:
|
|
431
|
+
|
|
432
|
+
```ruby
|
|
433
|
+
RobotLab::Memory.new.to_h
|
|
434
|
+
# => { data: {}, results: [], messages: [], custom: {} } -- no :session_id
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
Consumers must tolerate the missing key. `custom` is always present (it is
|
|
438
|
+
`{}` rather than nil when there are no custom keys).
|
|
439
|
+
|
|
238
440
|
### to_json
|
|
239
441
|
|
|
240
442
|
```ruby
|
|
241
443
|
memory.to_json # => String
|
|
242
444
|
```
|
|
243
445
|
|
|
446
|
+
Serializes `to_h`.
|
|
447
|
+
|
|
244
448
|
### from_hash
|
|
245
449
|
|
|
246
450
|
```ruby
|
|
247
|
-
memory = Memory.from_hash(hash)
|
|
451
|
+
memory = RobotLab::Memory.from_hash(hash)
|
|
248
452
|
```
|
|
249
453
|
|
|
250
|
-
|
|
454
|
+
Reconstructs `data`, `results`, `messages`, and `session_id`, then re-applies
|
|
455
|
+
every entry from `custom`. A fresh cache is created; subscriptions, backend
|
|
456
|
+
choice, and `network_name` are **not** restored.
|
|
251
457
|
|
|
252
|
-
### clone
|
|
458
|
+
### clone / dup
|
|
253
459
|
|
|
254
460
|
```ruby
|
|
255
461
|
new_memory = memory.clone
|
|
462
|
+
new_memory = memory.dup # alias for clone
|
|
256
463
|
```
|
|
257
464
|
|
|
258
|
-
Deep copy
|
|
465
|
+
Deep copy of `data` plus copies of `results`, `messages`, `session_id`, and all
|
|
466
|
+
non-reserved keys. The `enable_cache` setting and `network_name` are preserved.
|
|
467
|
+
Subscriptions are **not** copied — the clone starts with none. Copying custom
|
|
468
|
+
keys uses the internal non-reactive setter, so no notifications fire.
|
|
259
469
|
|
|
260
470
|
## Examples
|
|
261
471
|
|
|
@@ -297,11 +507,18 @@ network.memory.set(:sentiment, { score: 0.8, confidence: 0.95 })
|
|
|
297
507
|
|
|
298
508
|
# In robot B (reader, may run concurrently)
|
|
299
509
|
result = network.memory.get(:sentiment, wait: true) # Block indefinitely
|
|
300
|
-
result = network.memory.get(:sentiment, wait: 30) # Block up to 30s
|
|
510
|
+
result = network.memory.get(:sentiment, wait: 30) # Block up to 30s, then raise
|
|
301
511
|
|
|
302
|
-
# Multiple keys
|
|
512
|
+
# Multiple keys — the timeout applies PER MISSING KEY, so this can block 180s
|
|
303
513
|
results = network.memory.get(:sentiment, :entities, :keywords, wait: 60)
|
|
304
514
|
# => { sentiment: {...}, entities: [...], keywords: [...] }
|
|
515
|
+
|
|
516
|
+
# Treat a timeout as "not available"
|
|
517
|
+
sentiment = begin
|
|
518
|
+
network.memory.get(:sentiment, wait: 30)
|
|
519
|
+
rescue RobotLab::AwaitTimeout
|
|
520
|
+
nil
|
|
521
|
+
end
|
|
305
522
|
```
|
|
306
523
|
|
|
307
524
|
### Reactive Subscriptions
|
|
@@ -357,4 +574,5 @@ memory = Memory.from_hash(data)
|
|
|
357
574
|
## See Also
|
|
358
575
|
|
|
359
576
|
- [Memory Guide](../../guides/memory.md)
|
|
577
|
+
- [StateProxy](state.md) — the wrapper returned by `memory.data`
|
|
360
578
|
- [State Management Architecture](../../architecture/state-management.md)
|