robot_lab-durable 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 +2 -0
- data/.loki +10 -0
- data/.quality/reek_baseline.txt +0 -0
- data/.rubocop.yml +1 -173
- data/CHANGELOG.md +42 -0
- data/CLAUDE.md +65 -0
- data/README.md +65 -30
- data/Rakefile +47 -31
- data/docs/document_store_backend_design.md +4 -37
- data/docs/index.md +230 -25
- data/examples/01_day_trader.rb +92 -0
- data/examples/{33_stock_generator.rb → day_trader_lib/generator.rb} +6 -4
- data/examples/{33_stock_predictor.rb → day_trader_lib/predictor.rb} +92 -73
- data/lib/robot_lab/durable/adapter.rb +42 -0
- data/lib/robot_lab/durable/entry.rb +8 -39
- data/lib/robot_lab/durable/hook.rb +48 -0
- data/lib/robot_lab/durable/version.rb +1 -1
- data/lib/robot_lab/durable.rb +6 -8
- data/lib/robot_lab/recall_knowledge.rb +6 -10
- data/lib/robot_lab/record_knowledge.rb +7 -17
- metadata +29 -12
- data/lib/robot_lab/durable/learning.rb +0 -39
- data/lib/robot_lab/durable/reflector.rb +0 -47
- data/lib/robot_lab/durable/store.rb +0 -120
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 54a17c8e6e00177f47175470d75d785d95aa4322ee89f1dd89702b218491b2e8
|
|
4
|
+
data.tar.gz: 0d87cba5d51a23813f7592058daf4f4f3e12822d6545d03bbcb363cf8a9013b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bb2cd6073cbd35a76fed4d8a741171b40109d5c0739297879460b37d7a9141e2d4391247c2ff80118d661cc648380fbca5fa54f54afb34dce833f4ae2e6f1eee
|
|
7
|
+
data.tar.gz: ec9d1af50d43b74ef6826e2216173d990101028394b4960414c7ff64bded29301b13879c6816e1c37a257a03fcf4e3950d47fc499fd06c0d035cb323400a794d
|
data/.envrc
CHANGED
data/.loki
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# robot_lab-durable — cross-session persistent robot memory and learned behaviors.
|
|
3
|
+
|
|
4
|
+
import_up "repo_dev.loki"
|
|
5
|
+
|
|
6
|
+
class Tasks
|
|
7
|
+
@@gem_name ||= "robot_lab-durable".freeze
|
|
8
|
+
|
|
9
|
+
header "robot_lab-durable v#{gem_version} — cross-session persistent learning"
|
|
10
|
+
end
|
|
File without changes
|
data/.rubocop.yml
CHANGED
|
@@ -1,173 +1 @@
|
|
|
1
|
-
|
|
2
|
-
NewCops: enable
|
|
3
|
-
SuggestExtensions: false
|
|
4
|
-
TargetRubyVersion: 4.0
|
|
5
|
-
Exclude:
|
|
6
|
-
- 'examples/**/*'
|
|
7
|
-
- 'vendor/**/*'
|
|
8
|
-
- 'dead_code/**/*'
|
|
9
|
-
|
|
10
|
-
# ── Style: disabled cops ───────────────────────────────────────────────────
|
|
11
|
-
Style/StringLiterals:
|
|
12
|
-
Enabled: false
|
|
13
|
-
|
|
14
|
-
Style/StringLiteralsInInterpolation:
|
|
15
|
-
Enabled: false
|
|
16
|
-
|
|
17
|
-
Style/Documentation:
|
|
18
|
-
Enabled: false
|
|
19
|
-
|
|
20
|
-
# Ruby 4.0 freezes string literals by default
|
|
21
|
-
Style/FrozenStringLiteralComment:
|
|
22
|
-
Enabled: false
|
|
23
|
-
|
|
24
|
-
Style/IfUnlessModifier:
|
|
25
|
-
Enabled: false
|
|
26
|
-
|
|
27
|
-
Style/RescueModifier:
|
|
28
|
-
Enabled: false
|
|
29
|
-
|
|
30
|
-
Style/TrivialAccessors:
|
|
31
|
-
Enabled: false
|
|
32
|
-
|
|
33
|
-
Style/MultilineTernaryOperator:
|
|
34
|
-
Enabled: false
|
|
35
|
-
|
|
36
|
-
Style/SafeNavigation:
|
|
37
|
-
Enabled: false
|
|
38
|
-
|
|
39
|
-
Style/EmptyClassDefinition:
|
|
40
|
-
Enabled: false
|
|
41
|
-
|
|
42
|
-
Style/ClassAndModuleChildren:
|
|
43
|
-
Enabled: false
|
|
44
|
-
|
|
45
|
-
Style/RescueStandardError:
|
|
46
|
-
Enabled: false
|
|
47
|
-
|
|
48
|
-
Style/OneClassPerFile:
|
|
49
|
-
Enabled: false
|
|
50
|
-
|
|
51
|
-
# Both % and format/sprintf are acceptable
|
|
52
|
-
Style/FormatString:
|
|
53
|
-
Enabled: false
|
|
54
|
-
|
|
55
|
-
# String concatenation and interpolation are both acceptable
|
|
56
|
-
Style/StringConcatenation:
|
|
57
|
-
Enabled: false
|
|
58
|
-
|
|
59
|
-
# ── Layout ─────────────────────────────────────────────────────────────────
|
|
60
|
-
Layout/LineLength:
|
|
61
|
-
Max: 140
|
|
62
|
-
|
|
63
|
-
Layout/ExtraSpacing:
|
|
64
|
-
Enabled: false
|
|
65
|
-
|
|
66
|
-
Layout/HashAlignment:
|
|
67
|
-
Enabled: false
|
|
68
|
-
|
|
69
|
-
Layout/FirstHashElementIndentation:
|
|
70
|
-
Enabled: false
|
|
71
|
-
|
|
72
|
-
Layout/EmptyLineAfterGuardClause:
|
|
73
|
-
Enabled: false
|
|
74
|
-
|
|
75
|
-
# ── Naming ─────────────────────────────────────────────────────────────────
|
|
76
|
-
# Single-char params (c, e, n) are acceptable throughout
|
|
77
|
-
Naming/MethodParameterName:
|
|
78
|
-
Enabled: false
|
|
79
|
-
|
|
80
|
-
Naming/VariableNumber:
|
|
81
|
-
Exclude:
|
|
82
|
-
- 'test/**/*'
|
|
83
|
-
|
|
84
|
-
Naming/RescuedExceptionsVariableName:
|
|
85
|
-
Enabled: false
|
|
86
|
-
|
|
87
|
-
# set_results and similar explicit setters are clear and conventional
|
|
88
|
-
Naming/AccessorMethodName:
|
|
89
|
-
Enabled: false
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
# has_tool_calls? and similar are clear and conventional
|
|
93
|
-
Naming/PredicatePrefix:
|
|
94
|
-
Enabled: false
|
|
95
|
-
|
|
96
|
-
# Test helper methods don't need to follow predicate naming rules
|
|
97
|
-
Naming/PredicateMethod:
|
|
98
|
-
Exclude:
|
|
99
|
-
- 'test/**/*'
|
|
100
|
-
|
|
101
|
-
# ── Lint: relax noisy cops on intentional patterns ─────────────────────────
|
|
102
|
-
# Library and framework methods commonly accept args for API/documentation purposes
|
|
103
|
-
Lint/UnusedMethodArgument:
|
|
104
|
-
Enabled: false
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
Lint/EmptyBlock:
|
|
108
|
-
Exclude:
|
|
109
|
-
- 'test/**/*'
|
|
110
|
-
|
|
111
|
-
Lint/ConstantDefinitionInBlock:
|
|
112
|
-
Exclude:
|
|
113
|
-
- 'Rakefile'
|
|
114
|
-
- 'test/**/*'
|
|
115
|
-
|
|
116
|
-
# ── Gemspec ────────────────────────────────────────────────────────────────
|
|
117
|
-
Gemspec/DevelopmentDependencies:
|
|
118
|
-
EnforcedStyle: Gemfile
|
|
119
|
-
|
|
120
|
-
Gemspec/RequiredRubyVersion:
|
|
121
|
-
Enabled: false
|
|
122
|
-
|
|
123
|
-
Gemspec/OrderedDependencies:
|
|
124
|
-
Enabled: false
|
|
125
|
-
|
|
126
|
-
# ── Metrics ────────────────────────────────────────────────────────────────
|
|
127
|
-
# Framework-level code (routers, parsers, orchestrators) is inherently complex.
|
|
128
|
-
# Flog is the primary complexity gate — these RuboCop thresholds catch only
|
|
129
|
-
# egregious outliers without false-positiving every dispatch method.
|
|
130
|
-
|
|
131
|
-
Metrics/MethodLength:
|
|
132
|
-
Max: 35
|
|
133
|
-
CountAsOne:
|
|
134
|
-
- heredoc
|
|
135
|
-
- array
|
|
136
|
-
- hash
|
|
137
|
-
Exclude:
|
|
138
|
-
- 'test/**/*'
|
|
139
|
-
|
|
140
|
-
Metrics/AbcSize:
|
|
141
|
-
Max: 40
|
|
142
|
-
Exclude:
|
|
143
|
-
- 'test/**/*'
|
|
144
|
-
|
|
145
|
-
Metrics/ClassLength:
|
|
146
|
-
Max: 600
|
|
147
|
-
Exclude:
|
|
148
|
-
- 'test/**/*'
|
|
149
|
-
|
|
150
|
-
Metrics/ModuleLength:
|
|
151
|
-
Max: 200
|
|
152
|
-
Exclude:
|
|
153
|
-
- 'test/**/*'
|
|
154
|
-
|
|
155
|
-
Metrics/CyclomaticComplexity:
|
|
156
|
-
Max: 20
|
|
157
|
-
Exclude:
|
|
158
|
-
- 'test/**/*'
|
|
159
|
-
|
|
160
|
-
Metrics/PerceivedComplexity:
|
|
161
|
-
Max: 20
|
|
162
|
-
Exclude:
|
|
163
|
-
- 'test/**/*'
|
|
164
|
-
|
|
165
|
-
# Long method signatures with keyword args are a Ruby framework idiom
|
|
166
|
-
Metrics/ParameterLists:
|
|
167
|
-
Enabled: false
|
|
168
|
-
|
|
169
|
-
Metrics/BlockLength:
|
|
170
|
-
Exclude:
|
|
171
|
-
- 'Rakefile'
|
|
172
|
-
- '*.gemspec'
|
|
173
|
-
- 'test/**/*'
|
|
1
|
+
inherit_from: ../.rubocop-base.yml
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
### Added
|
|
4
|
+
- `.loki` Asgard task file: `test`, `rubocop`, `rubocop_fix`, `flog`, `flay`, `quality`, `build`, `install`, `release`, and `console` tasks via the Asgard task runner
|
|
5
|
+
- `flay_check` Rake task: structural code duplication gate (mass threshold 50); integrated into the `quality` Rake task
|
|
6
|
+
- `flay` and `minitest-reporters` gems added to development dependencies
|
|
7
|
+
- `test_output.txt`, `flay_output.txt`, `flog_output.txt`, and `rubocop_output.txt` added to `.gitignore`
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- `test/test_helper.rb`: test output redirected to `test_output.txt` via `$stdout` reassignment; `TerminalSummaryReporter` prints a single PASS/FAIL summary line to the terminal
|
|
11
|
+
- `Rakefile`: `rubocop` and `rubocop_fix` tasks removed (now owned by Asgard); `flay_check` integrated into the `quality` gate
|
|
12
|
+
|
|
13
|
+
## [0.3.0] - 2026-05-24
|
|
14
|
+
|
|
15
|
+
### Changed (breaking)
|
|
16
|
+
- **Backend replaced**: YAML file store replaced entirely by the `htm` gem (PostgreSQL + pgvector).
|
|
17
|
+
`robot_lab-durable` is now a thin adapter between `robot_lab` and `htm`.
|
|
18
|
+
- `Durable::Entry` is now a presenter built from HTM node hashes via `Entry.from_node(node)`.
|
|
19
|
+
Removed fields: `domain`, `confidence`, `use_count`. Added: `node_id`.
|
|
20
|
+
- `Durable::Hook` no longer manages a domain or seeds past knowledge into session memory.
|
|
21
|
+
`around_run` initialises a thread-local `Adapter` from `ctx.robot.name`; `on_learn` persists
|
|
22
|
+
new learnings via `adapter.record`. `Hook.current_adapter` returns the active adapter.
|
|
23
|
+
- `RecallKnowledge` and `RecordKnowledge` now delegate to `Hook.current_adapter` instead of a
|
|
24
|
+
YAML store. The `domain:` parameter is removed from both tools.
|
|
25
|
+
- `Gemfile` updated: depends on published `htm` gem and local `robot_lab` path.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- `Durable::Adapter` — wraps `HTM.new(robot_name:)`, exposes `recall(query:, ...)` and
|
|
29
|
+
`record(content:, reasoning:, category:)`. HTM deduplicates by content hash, eliminating
|
|
30
|
+
the need for double-write prevention.
|
|
31
|
+
- Full test suite for `Durable::Adapter` (`test/robot_lab/durable/adapter_test.rb`) using a
|
|
32
|
+
`FakeHTM` stand-in — no PostgreSQL required.
|
|
33
|
+
- `examples/01_day_trader.rb` — demo launcher that spawns the generator and predictor as
|
|
34
|
+
subprocesses and relays their output with colour-coded `[GEN ]`/`[PRED]` prefixes.
|
|
35
|
+
- `examples/day_trader_lib/generator.rb` — XYZZY fake stock price stream via Redis (GBM model).
|
|
36
|
+
- `examples/day_trader_lib/predictor.rb` — SMA+EMA ensemble tuned by a durable agent;
|
|
37
|
+
prints `✓ GOOD / ✗ MISS` verdict after each prediction window.
|
|
38
|
+
|
|
39
|
+
### Removed
|
|
40
|
+
- `Durable::Store` (YAML-backed per-domain file store).
|
|
41
|
+
- `Durable::Reflector` (end-of-run confidence promoter).
|
|
42
|
+
- `domain` concept throughout — HTM uses `robot_name` for scoping.
|
|
43
|
+
- `skip_persist` mechanism — HTM SHA-256 content deduplication makes it unnecessary.
|
|
44
|
+
|
|
3
45
|
## [0.2.1] - 2026-05-19
|
|
4
46
|
|
|
5
47
|
### Added
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## What This Gem Does
|
|
6
|
+
|
|
7
|
+
`robot_lab-durable` gives RobotLab robots cross-session persistent memory backed by the `htm` gem (Hierarchical Temporal Memory). Knowledge is recorded by robots during a session and recalled in future sessions using hybrid semantic search (vector + full-text).
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bundle exec rake test # Run full test suite
|
|
13
|
+
ruby -Ilib:test test/<file> # Run a single test file
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Architecture
|
|
17
|
+
|
|
18
|
+
All source lives under `lib/robot_lab/durable/` plus two tool files at `lib/robot_lab/`.
|
|
19
|
+
|
|
20
|
+
**`Adapter`** (`durable/adapter.rb`) — Wraps an `HTM` instance for one robot. Two operations:
|
|
21
|
+
- `recall(query:, limit:, strategy:)` — calls `htm.recall` and maps nodes to `Entry` structs. Strategies: `:vector`, `:fulltext`, `:hybrid` (default)
|
|
22
|
+
- `record(content:, reasoning:, category:)` — calls `htm.remember` with metadata hash
|
|
23
|
+
|
|
24
|
+
**`Entry`** (`durable/entry.rb`) — `Data.define(:node_id, :content, :reasoning, :category, :created_at)`. Immutable value object. `category` is always a Symbol. Built via `Entry.from_node(node)` from raw HTM node hashes.
|
|
25
|
+
|
|
26
|
+
**`Hook`** (`durable/hook.rb`) — Subclass of `RobotLab::Hook` with `namespace = :durable`. Manages a per-thread `Adapter` instance:
|
|
27
|
+
- `around_run` — creates an `Adapter` for the robot, stores it in `Thread.current`, tears it down in `ensure`
|
|
28
|
+
- `on_learn` — called by the `:learn` hook family when the robot learns something; persists to HTM if `ctx.stored` is true
|
|
29
|
+
|
|
30
|
+
**`RecallKnowledge`** (`recall_knowledge.rb`) — `RobotLab::Tool` subclass. Takes a `query` param, calls `Hook.current_adapter.recall`, formats results for the LLM. Returns "No relevant past knowledge found" when empty — instructs the robot to skip rather than guess.
|
|
31
|
+
|
|
32
|
+
**`RecordKnowledge`** (`record_knowledge.rb`) — `RobotLab::Tool` subclass. Takes `content`, `reasoning`, `category` params. Calls `adapter.record` then `robot.learn(content)` to update in-session memory immediately. HTM deduplicates by content hash so double-writes are harmless.
|
|
33
|
+
|
|
34
|
+
## Enabling
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
require 'robot_lab'
|
|
38
|
+
require 'robot_lab/durable'
|
|
39
|
+
|
|
40
|
+
robot = RobotLab.build(
|
|
41
|
+
name: "advisor",
|
|
42
|
+
local_tools: [RobotLab::RecallKnowledge, RobotLab::RecordKnowledge]
|
|
43
|
+
)
|
|
44
|
+
robot.on(RobotLab::Durable::Hook)
|
|
45
|
+
result = robot.run("Remember my preference for concise answers.")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Valid Categories
|
|
49
|
+
|
|
50
|
+
`fact`, `preference`, `pattern`, `correction`, `observation`
|
|
51
|
+
|
|
52
|
+
The LLM is free to choose — these are stored as metadata strings and returned as Symbols in `Entry#category`.
|
|
53
|
+
|
|
54
|
+
## Key Constraints
|
|
55
|
+
|
|
56
|
+
- The `Adapter` is scoped to `Thread.current` — it is NOT available across Ractors or Fibers.
|
|
57
|
+
- `RecordKnowledge` calls `robot.learn` internally; this triggers the `:learn` hook which calls `on_learn`. HTM deduplication prevents a double-write.
|
|
58
|
+
- `Hook` loads only when `RobotLab::Hook` is defined; `RecallKnowledge`/`RecordKnowledge` load only when `RobotLab::Tool` is defined. Always `require 'robot_lab'` before `require 'robot_lab/durable'`.
|
|
59
|
+
- The `htm` gem manages its own storage backend (PostgreSQL with vector extensions). Ensure the HTM database is configured before use.
|
|
60
|
+
|
|
61
|
+
## Testing
|
|
62
|
+
|
|
63
|
+
- Minitest with SimpleCov (branch coverage tracked, no minimum threshold enforced yet)
|
|
64
|
+
- Tests stub `HTM` — do not use a real database in unit tests
|
|
65
|
+
- Coverage baseline: ~83% line / ~44% branch
|
data/README.md
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
# robot_lab-durable
|
|
2
2
|
|
|
3
|
-
Cross-session durable
|
|
3
|
+
Cross-session durable memory for the [RobotLab](https://github.com/MadBomber/robot_lab) LLM agent framework, backed by the [htm gem](https://madbomber.github.io/htm) (PostgreSQL + pgvector).
|
|
4
4
|
|
|
5
5
|
> [!CAUTION]
|
|
6
6
|
> This gem is under active development. APIs may change without notice.
|
|
7
7
|
|
|
8
8
|
## What it provides
|
|
9
9
|
|
|
10
|
-
- **`Durable::
|
|
11
|
-
- **`Durable::
|
|
12
|
-
- **`Durable::
|
|
13
|
-
- **`
|
|
14
|
-
- **`
|
|
15
|
-
|
|
10
|
+
- **`Durable::Adapter`** — thin wrapper around an `HTM` instance; scoped to a robot by name. Exposes `record` and `recall` methods.
|
|
11
|
+
- **`Durable::Entry`** — presenter over raw HTM node hashes returned by recall. Provides typed fields: `node_id`, `content`, `reasoning`, `category`, `created_at`.
|
|
12
|
+
- **`Durable::Hook`** — RobotLab hook handler that wires the `Adapter` lifecycle into `around_run` and `on_learn` automatically.
|
|
13
|
+
- **`RecallKnowledge`** tool — lets a robot query its durable memory before answering.
|
|
14
|
+
- **`RecordKnowledge`** tool — lets a robot persist new knowledge during a session.
|
|
15
|
+
|
|
16
|
+
## Prerequisites
|
|
17
|
+
|
|
18
|
+
- PostgreSQL with the pgvector extension enabled.
|
|
19
|
+
- The `htm` gem — see documentation at https://madbomber.github.io/htm.
|
|
20
|
+
- The `robot_lab` gem.
|
|
16
21
|
|
|
17
22
|
## Installation
|
|
18
23
|
|
|
@@ -23,6 +28,12 @@ gem "robot_lab"
|
|
|
23
28
|
gem "robot_lab-durable"
|
|
24
29
|
```
|
|
25
30
|
|
|
31
|
+
Then run:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
bundle install
|
|
35
|
+
```
|
|
36
|
+
|
|
26
37
|
## Quick Example
|
|
27
38
|
|
|
28
39
|
```ruby
|
|
@@ -30,53 +41,77 @@ require "robot_lab"
|
|
|
30
41
|
require "robot_lab/durable"
|
|
31
42
|
|
|
32
43
|
robot = RobotLab.build(
|
|
33
|
-
name:
|
|
34
|
-
system_prompt: "You are a financial advisor
|
|
35
|
-
|
|
36
|
-
learn_domain: "finance"
|
|
44
|
+
name: "advisor",
|
|
45
|
+
system_prompt: "You are a financial advisor.",
|
|
46
|
+
local_tools: [RobotLab::RecallKnowledge, RobotLab::RecordKnowledge]
|
|
37
47
|
)
|
|
48
|
+
robot.on(RobotLab::Durable::Hook)
|
|
38
49
|
|
|
39
|
-
# RecallKnowledge and RecordKnowledge tools are automatically available.
|
|
40
|
-
# At the end of each run, the Reflector promotes learned facts to
|
|
41
|
-
# ~/.robot_lab/durable/finance.yml for use in future sessions.
|
|
42
50
|
result = robot.run("What do you know about my risk tolerance?")
|
|
43
51
|
puts result.last_text_content
|
|
44
52
|
```
|
|
45
53
|
|
|
54
|
+
No `domain:` argument is required. HTM scopes all storage to the robot's `name` automatically.
|
|
55
|
+
|
|
46
56
|
## How It Works
|
|
47
57
|
|
|
48
|
-
|
|
58
|
+
### Hook lifecycle
|
|
49
59
|
|
|
50
|
-
|
|
51
|
-
- **`RecordKnowledge`** — writes new knowledge entries during a session
|
|
60
|
+
`RobotLab::Durable::Hook` is registered via `robot.on(...)` and plugs into two lifecycle events:
|
|
52
61
|
|
|
53
|
-
|
|
62
|
+
- **`around_run`** — creates a thread-local `Adapter` instance keyed to `ctx.robot.name`, makes it available to tools for the duration of the run, and clears it in `ensure` so state never leaks across run boundaries.
|
|
63
|
+
- **`on_learn`** — fires after each new session learning is stored in memory. When a durable session is active, persists the text via `adapter.record(content:, category: :observation)`.
|
|
54
64
|
|
|
55
|
-
|
|
65
|
+
### Adapter
|
|
56
66
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
`Durable::Adapter` wraps an `HTM` instance and provides two methods:
|
|
68
|
+
|
|
69
|
+
| Method | Description |
|
|
70
|
+
|--------|-------------|
|
|
71
|
+
| `record(content:, reasoning: nil, category: 'fact')` | Stores a memory entry. HTM deduplicates by SHA-256 content hash, so calling `record` with identical content is safe. |
|
|
72
|
+
| `recall(query:, limit: 20, strategy: :hybrid)` | Searches stored memory and returns an array of `Entry` objects. |
|
|
73
|
+
| `htm` | Direct access to the underlying `HTM` instance for advanced use. |
|
|
63
74
|
|
|
64
|
-
|
|
75
|
+
### Entry
|
|
65
76
|
|
|
66
|
-
|
|
77
|
+
`Durable::Entry` is a presenter built via `Entry.from_node(node)` over the raw hashes returned by `HTM#recall`. It exposes:
|
|
67
78
|
|
|
68
|
-
|
|
79
|
+
| Field | Type | Description |
|
|
80
|
+
|-------|------|-------------|
|
|
81
|
+
| `node_id` | String | Unique HTM node identifier |
|
|
82
|
+
| `content` | String | The stored knowledge text |
|
|
83
|
+
| `reasoning` | String / nil | Optional rationale recorded at write time |
|
|
84
|
+
| `category` | Symbol | `:fact`, `:observation`, or any custom value |
|
|
85
|
+
| `created_at` | Time | When the entry was first stored |
|
|
69
86
|
|
|
70
|
-
|
|
87
|
+
## Tools
|
|
88
|
+
|
|
89
|
+
### RecallKnowledge
|
|
90
|
+
|
|
91
|
+
Calls `Hook.current_adapter.recall(query:)` with the query text provided by the LLM and returns formatted entries. Uses the `:hybrid` strategy by default.
|
|
92
|
+
|
|
93
|
+
### RecordKnowledge
|
|
94
|
+
|
|
95
|
+
Calls `adapter.record(content:, reasoning:, category:)` to persist a new memory, then calls `robot.learn(content)` to update session context. Because HTM deduplicates by content hash, no explicit double-write prevention is necessary.
|
|
96
|
+
|
|
97
|
+
## Day Trader Demo
|
|
98
|
+
|
|
99
|
+
`examples/01_day_trader.rb` demonstrates cross-session durable memory in action. It spawns a GBM stock price generator and an SMA+EMA ensemble predictor as subprocesses. Output is colour-coded `[GEN ]` / `[PRED]` per process, with a GOOD/MISS verdict printed for each prediction window. The durable agent tunes its prediction parameters across sessions, accumulating knowledge about what works.
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
ruby examples/01_day_trader.rb
|
|
103
|
+
```
|
|
71
104
|
|
|
72
105
|
## Links
|
|
73
106
|
|
|
74
107
|
- [RobotLab Core](https://github.com/MadBomber/robot_lab)
|
|
108
|
+
- [HTM gem docs](https://madbomber.github.io/htm)
|
|
75
109
|
- [RubyGems](https://rubygems.org/gems/robot_lab-durable)
|
|
110
|
+
- [GitHub](https://github.com/MadBomber/robot_lab-durable)
|
|
76
111
|
|
|
77
112
|
## License
|
|
78
113
|
|
|
79
|
-
MIT License
|
|
114
|
+
MIT License — Copyright (c) 2025 Dewayne VanHoozer
|
|
80
115
|
|
|
81
116
|
## Contributing
|
|
82
117
|
|
data/Rakefile
CHANGED
|
@@ -24,16 +24,6 @@ task :test_file, [:file] do |_t, args|
|
|
|
24
24
|
ruby "test/#{args[:file]}"
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
desc 'Check code style with RuboCop'
|
|
28
|
-
task :rubocop do
|
|
29
|
-
sh 'bundle exec rubocop'
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
desc 'Auto-correct RuboCop offenses'
|
|
33
|
-
task :rubocop_fix do
|
|
34
|
-
sh 'bundle exec rubocop -a'
|
|
35
|
-
end
|
|
36
|
-
|
|
37
27
|
desc 'Check code complexity with Flog (warn >=20, fail >=50)'
|
|
38
28
|
task :flog_check do
|
|
39
29
|
require 'flog'
|
|
@@ -71,36 +61,62 @@ task :flog_check do
|
|
|
71
61
|
end
|
|
72
62
|
end
|
|
73
63
|
|
|
74
|
-
desc '
|
|
75
|
-
task :
|
|
76
|
-
|
|
64
|
+
desc 'Check for structural code duplication with Flay (mass >= 50)'
|
|
65
|
+
task :flay_check do
|
|
66
|
+
require 'flay'
|
|
77
67
|
|
|
78
|
-
|
|
79
|
-
puts 'Quality Gate: Tests + Coverage'
|
|
80
|
-
puts '=' * 60
|
|
81
|
-
results[:tests] = system('bundle exec rake test') ? :pass : :fail
|
|
68
|
+
mass_threshold = 50
|
|
82
69
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
results[:rubocop] = system('bundle exec rubocop') ? :pass : :fail
|
|
70
|
+
flay = Flay.new({ mass: mass_threshold, diff: false, verbose: false, summary: false, timeout: 60 })
|
|
71
|
+
flay.process(*Dir.glob('lib/**/*.rb'))
|
|
72
|
+
flay.analyze
|
|
87
73
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
74
|
+
if flay.hashes.empty?
|
|
75
|
+
puts "\nFlay: no structural duplication detected (mass >= #{mass_threshold})"
|
|
76
|
+
else
|
|
77
|
+
puts "\nFlay found structural duplication (mass >= #{mass_threshold}):"
|
|
78
|
+
flay.report
|
|
79
|
+
abort "\nFlay quality gate failed: #{flay.hashes.length} pattern(s) detected"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
desc 'Run all quality checks: tests (with coverage), RuboCop, Flog, and Flay'
|
|
84
|
+
task :quality do
|
|
85
|
+
gates = [
|
|
86
|
+
['Tests + Coverage', 'bundle exec rake test'],
|
|
87
|
+
['RuboCop', 'bundle exec rubocop'],
|
|
88
|
+
['Flog Complexity', 'bundle exec rake flog_check'],
|
|
89
|
+
['Flay Duplication', 'bundle exec rake flay_check']
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
results = gates.map do |label, command|
|
|
93
|
+
puts "\n#{'=' * 60}"
|
|
94
|
+
puts "Quality Gate: #{label}"
|
|
95
|
+
puts '=' * 60
|
|
96
|
+
[label, system(command) ? :pass : :fail]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
green = ->(s) { "\e[32m#{s}\e[0m" }
|
|
100
|
+
red = ->(s) { "\e[31m#{s}\e[0m" }
|
|
101
|
+
width = results.map { |label, _| label.length }.max
|
|
92
102
|
|
|
93
103
|
puts "\n#{'=' * 60}"
|
|
94
|
-
puts 'Quality Summary'
|
|
104
|
+
puts 'Quality Gate Summary'
|
|
95
105
|
puts '=' * 60
|
|
96
|
-
results.each do |
|
|
97
|
-
|
|
98
|
-
puts " [#{
|
|
106
|
+
results.each do |label, status|
|
|
107
|
+
badge = status == :pass ? green.call('PASS') : red.call('FAIL')
|
|
108
|
+
puts " [#{badge}] #{label.ljust(width)}"
|
|
99
109
|
end
|
|
110
|
+
puts '-' * 60
|
|
111
|
+
|
|
112
|
+
passed = results.count { |_, s| s == :pass }
|
|
113
|
+
failed = results.count { |_, s| s == :fail }
|
|
114
|
+
tally = "#{passed} passed, #{failed} failed"
|
|
115
|
+
puts " #{failed.zero? ? green.call(tally) : red.call(tally)}"
|
|
100
116
|
puts '=' * 60
|
|
101
117
|
|
|
102
|
-
abort "\
|
|
103
|
-
puts "\
|
|
118
|
+
abort "\n#{red.call('Quality gate failed.')}" unless failed.zero?
|
|
119
|
+
puts "\n#{green.call('All quality gates passed.')}"
|
|
104
120
|
end
|
|
105
121
|
|
|
106
122
|
namespace :docs do
|
|
@@ -1,42 +1,9 @@
|
|
|
1
1
|
# Delegate Storage to robot_lab-document_store — Design Discussion
|
|
2
2
|
|
|
3
3
|
**Date:** 2026-05-14
|
|
4
|
-
**
|
|
4
|
+
**Original status:** Parked — resume when time allows
|
|
5
|
+
**Current status:** Superseded — see [docs/index.md](index.md)
|
|
5
6
|
|
|
6
|
-
|
|
7
|
+
The document_store delegation path was abandoned. In v0.3.0, `robot_lab-durable` became a direct adapter over the [`htm` gem](https://madbomber.github.io/htm) (PostgreSQL + pgvector), replacing the YAML store, the `Durable::Store` class, `Durable::Reflector`, and all domain/confidence concepts entirely. The open questions raised here — search semantics, structured metadata, backend versioning — are resolved by HTM's native hybrid recall and metadata hash support.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
## The Vision
|
|
11
|
-
|
|
12
|
-
Once `robot_lab-document_store` gains a `DocumentStore::FileSystem` backend, durable should drop its custom `Store` class and delegate physical storage to it. Durable retains its own concerns:
|
|
13
|
-
|
|
14
|
-
- `Entry` — immutable value object with confidence scoring, category, domain, use_count
|
|
15
|
-
- `Reflector` — end-of-session promoter that pushes session learnings into the store
|
|
16
|
-
- `Learning` — mixin included into `Robot` when robot_lab is present
|
|
17
|
-
- `RecallKnowledge` / `RecordKnowledge` — LLM tools that interact with the store
|
|
18
|
-
|
|
19
|
-
## What Changes in This Gem
|
|
20
|
-
|
|
21
|
-
1. Add `robot_lab-document_store` as a runtime dependency in the gemspec.
|
|
22
|
-
2. Remove `lib/robot_lab/durable/store.rb`.
|
|
23
|
-
3. Wire `Learning#setup_durable_learning` to instantiate a `DocumentStore::FileSystem` instead of `Durable::Store`.
|
|
24
|
-
4. Adapt `Reflector` and the two tools to call the `DocumentStore` interface (`store`, `search`, `delete`, etc.) rather than the old `Store` API.
|
|
25
|
-
5. Handle `Entry` serialization — since `DocumentStore` stores raw text by key, durable will serialize `Entry` fields into text (or use a metadata hash if `FileSystem` supports it — see open questions).
|
|
26
|
-
|
|
27
|
-
## Open Questions (shared with robot_lab-document_store)
|
|
28
|
-
|
|
29
|
-
1. **Search semantics.** `DocumentStore::Memory` uses embedding-based cosine similarity; `DocumentStore::FileSystem` would use keyword matching. Should the interface declare its search capability, or do callers accept whatever the backend provides?
|
|
30
|
-
|
|
31
|
-
2. **Structured vs raw text.** `Entry` carries structured fields (confidence, category, domain, use_count). Options:
|
|
32
|
-
- Durable serializes all fields into the stored text string; deserializes on recall.
|
|
33
|
-
- `DocumentStore::FileSystem` supports an optional `meta:` Hash alongside text, which durable populates with `Entry` fields.
|
|
34
|
-
|
|
35
|
-
## Versioning
|
|
36
|
-
|
|
37
|
-
- `robot_lab-document_store` must ship `DocumentStore::FileSystem` first — that is a v0.3.0 breaking change for that gem (v0.2.1 is the current release).
|
|
38
|
-
- This gem (`robot_lab-durable`) then bumps to v0.3.0 once it drops `Store` and depends on document_store.
|
|
39
|
-
|
|
40
|
-
## See Also
|
|
41
|
-
|
|
42
|
-
`robot_lab-document_store/docs/pluggable_backends_design.md` — the full backend architecture design including the `DocumentStore` abstract interface and implementation plan.
|
|
9
|
+
For the current architecture, see [docs/index.md](index.md) and the [HTM gem documentation](https://madbomber.github.io/htm).
|