robot_lab-durable 0.1.0 → 0.2.1
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/.rubocop.yml +173 -0
- data/CHANGELOG.md +14 -0
- data/Rakefile +111 -3
- data/docs/document_store_backend_design.md +42 -0
- data/lib/robot_lab/durable/entry.rb +20 -20
- data/lib/robot_lab/durable/reflector.rb +5 -5
- data/lib/robot_lab/durable/store.rb +9 -8
- data/lib/robot_lab/durable/version.rb +1 -1
- data/lib/robot_lab/durable.rb +11 -9
- data/lib/robot_lab/recall_knowledge.rb +8 -8
- data/lib/robot_lab/record_knowledge.rb +12 -11
- metadata +7 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b576c8ce4dc5ca5b4741cf6b7d9a95b0b653b9f54ab15aa02e0114a2e7cf43d4
|
|
4
|
+
data.tar.gz: 5079f8bd4f6a63fdcc9938da446db749ca1e93ab844aa5d47c4a8fbd025d189f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7834ab27785b8f43f52e67dad700dd9a5a886cb0fb265d84e46b4cc79c0e123b5181847d17e87260400d6c3db3ac2212507ae7205186f684a6b8962270f49b57
|
|
7
|
+
data.tar.gz: cc57c77b81eaf328a07d167dc5d053dff56599935e51ada19fc50ac39bb73a73c1f63c28d387d77e3380693871b20e4e33c787cd9a2c39cd6d8734add3e8f19f
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
AllCops:
|
|
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/**/*'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.1] - 2026-05-19
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `Durable::Entry` — immutable `Data.define` value object with `content`, `confidence`, `category`, `domain`, `use_count`, `created_at`, and `updated_at` fields
|
|
7
|
+
- `Durable::Store` — YAML-backed, file-locked per-domain knowledge persistence in `~/.robot_lab/durable/`
|
|
8
|
+
- `Durable::Reflector` — promotes session-level learnings into the durable store at end-of-run with confidence scoring and deduplication
|
|
9
|
+
- `Durable::Learning` mixin — included into `RobotLab::Robot` when `learn: true` and `learn_domain:` constructor params are set
|
|
10
|
+
- `RecallKnowledge` tool — lets robots query the durable store before making decisions
|
|
11
|
+
- `RecordKnowledge` tool — lets robots write new knowledge entries during a session
|
|
12
|
+
- Design document for future `DocumentStore::FileSystem` backend integration (`docs/document_store_backend_design.md`)
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Version synchronized with robot_lab core 0.2.1
|
|
16
|
+
|
|
3
17
|
## [0.1.0] - 2026-05-07
|
|
4
18
|
|
|
5
19
|
- Initial release
|
data/Rakefile
CHANGED
|
@@ -1,8 +1,116 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'bundler/gem_tasks'
|
|
4
|
+
require 'rake/testtask'
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Rake::TestTask.new(:test) do |t|
|
|
7
|
+
t.libs << 'test'
|
|
8
|
+
t.libs << 'lib'
|
|
9
|
+
t.test_files = FileList['test/**/*_test.rb', 'test/**/test_*.rb'].exclude('**/*_helper.rb')
|
|
10
|
+
t.verbose = true
|
|
11
|
+
t.ruby_opts << '-rtest_helper'
|
|
12
|
+
end
|
|
7
13
|
|
|
8
14
|
task default: :test
|
|
15
|
+
|
|
16
|
+
desc 'Run tests with verbose output'
|
|
17
|
+
task :test_verbose do
|
|
18
|
+
ENV['TESTOPTS'] = '--verbose'
|
|
19
|
+
Rake::Task[:test].invoke
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc 'Run a single test file'
|
|
23
|
+
task :test_file, [:file] do |_t, args|
|
|
24
|
+
ruby "test/#{args[:file]}"
|
|
25
|
+
end
|
|
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
|
+
desc 'Check code complexity with Flog (warn >=20, fail >=50)'
|
|
38
|
+
task :flog_check do
|
|
39
|
+
require 'flog'
|
|
40
|
+
|
|
41
|
+
method_warn = 20.0
|
|
42
|
+
method_fail = 50.0
|
|
43
|
+
|
|
44
|
+
flogger = Flog.new(all: true)
|
|
45
|
+
flogger.flog(*Dir.glob('lib/**/*.rb'))
|
|
46
|
+
|
|
47
|
+
warnings = []
|
|
48
|
+
failures = []
|
|
49
|
+
|
|
50
|
+
flogger.each_by_score do |method, score|
|
|
51
|
+
next if method.end_with?('#none')
|
|
52
|
+
|
|
53
|
+
if score > method_fail
|
|
54
|
+
failures << "#{format('%.1f', score)}: #{method}"
|
|
55
|
+
elsif score > method_warn
|
|
56
|
+
warnings << "#{format('%.1f', score)}: #{method}"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
unless warnings.empty?
|
|
61
|
+
puts "\nFlog warnings (#{method_warn}–#{method_fail}) — target for future refactoring:"
|
|
62
|
+
warnings.each { |v| puts " #{v}" }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if failures.empty?
|
|
66
|
+
puts "\nFlog: no methods exceed the failure threshold (>=#{method_fail})"
|
|
67
|
+
else
|
|
68
|
+
puts "\nFlog failures (>=#{method_fail}) — must be refactored:"
|
|
69
|
+
failures.each { |v| puts " #{v}" }
|
|
70
|
+
abort "\nFlog quality gate failed: #{failures.size} method(s) exceed #{method_fail}"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
desc 'Run all quality checks: tests (with coverage), RuboCop, and Flog'
|
|
75
|
+
task :quality do
|
|
76
|
+
results = {}
|
|
77
|
+
|
|
78
|
+
puts "\n#{'=' * 60}"
|
|
79
|
+
puts 'Quality Gate: Tests + Coverage'
|
|
80
|
+
puts '=' * 60
|
|
81
|
+
results[:tests] = system('bundle exec rake test') ? :pass : :fail
|
|
82
|
+
|
|
83
|
+
puts "\n#{'=' * 60}"
|
|
84
|
+
puts 'Quality Gate: RuboCop'
|
|
85
|
+
puts '=' * 60
|
|
86
|
+
results[:rubocop] = system('bundle exec rubocop') ? :pass : :fail
|
|
87
|
+
|
|
88
|
+
puts "\n#{'=' * 60}"
|
|
89
|
+
puts 'Quality Gate: Flog Complexity'
|
|
90
|
+
puts '=' * 60
|
|
91
|
+
results[:flog] = system('bundle exec rake flog_check') ? :pass : :fail
|
|
92
|
+
|
|
93
|
+
puts "\n#{'=' * 60}"
|
|
94
|
+
puts 'Quality Summary'
|
|
95
|
+
puts '=' * 60
|
|
96
|
+
results.each do |gate, status|
|
|
97
|
+
icon = status == :pass ? 'PASS' : 'FAIL'
|
|
98
|
+
puts " [#{icon}] #{gate}"
|
|
99
|
+
end
|
|
100
|
+
puts '=' * 60
|
|
101
|
+
|
|
102
|
+
abort "\nQuality gate failed" if results.values.any?(:fail)
|
|
103
|
+
puts "\nAll quality gates passed."
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
namespace :docs do
|
|
107
|
+
desc 'Build MkDocs documentation'
|
|
108
|
+
task :build do
|
|
109
|
+
sh 'mkdocs build'
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
desc 'Serve MkDocs documentation locally on http://localhost:8000'
|
|
113
|
+
task :serve do
|
|
114
|
+
sh 'mkdocs serve'
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Delegate Storage to robot_lab-document_store — Design Discussion
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-05-14
|
|
4
|
+
**Status:** Parked — resume when time allows
|
|
5
|
+
|
|
6
|
+
## The Problem
|
|
7
|
+
|
|
8
|
+
`robot_lab-durable` currently maintains its own `Store` class: a YAML-backed, file-locked, keyword-search storage layer in `lib/robot_lab/durable/store.rb`. This is duplicated effort — `robot_lab-document_store` is intended to be the canonical pluggable storage abstraction for the robot_lab ecosystem.
|
|
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.
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module RobotLab
|
|
4
4
|
module Durable
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
MAX_CONFIDENCE = 1.0
|
|
5
|
+
CONFIDENCE_INCREMENT = 0.1
|
|
6
|
+
MAX_CONFIDENCE = 1.0
|
|
8
7
|
|
|
8
|
+
Entry = Data.define(:content, :reasoning, :category, :domain, :confidence, :use_count, :created_at, :updated_at) do
|
|
9
9
|
# Return a new Entry with confidence incremented and use_count bumped.
|
|
10
10
|
def confirm
|
|
11
11
|
new_confidence = [confidence + CONFIDENCE_INCREMENT, MAX_CONFIDENCE].min
|
|
12
12
|
with(
|
|
13
13
|
confidence: new_confidence.round(10),
|
|
14
|
-
use_count:
|
|
14
|
+
use_count: use_count + 1,
|
|
15
15
|
updated_at: Time.now.iso8601
|
|
16
16
|
)
|
|
17
17
|
end
|
|
@@ -19,14 +19,14 @@ module RobotLab
|
|
|
19
19
|
# Serialize to a plain Hash with string keys (safe for YAML round-trip).
|
|
20
20
|
def to_h
|
|
21
21
|
{
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
'content' => content,
|
|
23
|
+
'reasoning' => reasoning,
|
|
24
|
+
'category' => category.to_s,
|
|
25
|
+
'domain' => domain,
|
|
26
|
+
'confidence' => confidence,
|
|
27
|
+
'use_count' => use_count,
|
|
28
|
+
'created_at' => created_at,
|
|
29
|
+
'updated_at' => updated_at
|
|
30
30
|
}
|
|
31
31
|
end
|
|
32
32
|
|
|
@@ -34,14 +34,14 @@ module RobotLab
|
|
|
34
34
|
def self.from_h(hash)
|
|
35
35
|
h = hash.transform_keys(&:to_s)
|
|
36
36
|
new(
|
|
37
|
-
content:
|
|
38
|
-
reasoning:
|
|
39
|
-
category:
|
|
40
|
-
domain:
|
|
41
|
-
confidence: h[
|
|
42
|
-
use_count:
|
|
43
|
-
created_at: h[
|
|
44
|
-
updated_at: h[
|
|
37
|
+
content: h['content'],
|
|
38
|
+
reasoning: h['reasoning'],
|
|
39
|
+
category: h['category']&.to_sym,
|
|
40
|
+
domain: h['domain'],
|
|
41
|
+
confidence: h['confidence'].to_f,
|
|
42
|
+
use_count: h['use_count'].to_i,
|
|
43
|
+
created_at: h['created_at'],
|
|
44
|
+
updated_at: h['updated_at']
|
|
45
45
|
)
|
|
46
46
|
end
|
|
47
47
|
end
|
|
@@ -22,12 +22,12 @@ module RobotLab
|
|
|
22
22
|
now = Time.now.iso8601
|
|
23
23
|
@store.record(
|
|
24
24
|
Entry.new(
|
|
25
|
-
content:
|
|
26
|
-
reasoning:
|
|
27
|
-
category:
|
|
28
|
-
domain:
|
|
25
|
+
content: text,
|
|
26
|
+
reasoning: 'Observed during session (auto-promoted by Reflector)',
|
|
27
|
+
category: :pattern,
|
|
28
|
+
domain: @domain,
|
|
29
29
|
confidence: 0.1,
|
|
30
|
-
use_count:
|
|
30
|
+
use_count: 0,
|
|
31
31
|
created_at: now,
|
|
32
32
|
updated_at: now
|
|
33
33
|
)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'fileutils'
|
|
5
5
|
|
|
6
6
|
module RobotLab
|
|
7
7
|
module Durable
|
|
8
8
|
class Store
|
|
9
|
-
DEFAULT_PATH = File.join(Dir.home,
|
|
9
|
+
DEFAULT_PATH = File.join(Dir.home, '.robot_lab', 'durable')
|
|
10
10
|
|
|
11
11
|
MIN_WORD_LENGTH = 3
|
|
12
12
|
|
|
@@ -77,13 +77,13 @@ module RobotLab
|
|
|
77
77
|
file = domain_file(domain)
|
|
78
78
|
return [] unless File.exist?(file)
|
|
79
79
|
|
|
80
|
-
raw = Array(YAML.
|
|
80
|
+
raw = Array(YAML.safe_load_file(file) || [])
|
|
81
81
|
raw.map { |h| Entry.from_h(h) }
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
def load_all
|
|
85
|
-
Dir.glob(File.join(@path,
|
|
86
|
-
raw = Array(YAML.
|
|
85
|
+
Dir.glob(File.join(@path, '*.yaml')).flat_map do |file|
|
|
86
|
+
raw = Array(YAML.safe_load_file(file) || [])
|
|
87
87
|
raw.map { |h| Entry.from_h(h) }
|
|
88
88
|
end
|
|
89
89
|
end
|
|
@@ -97,18 +97,19 @@ module RobotLab
|
|
|
97
97
|
entries = load_domain(entry.domain)
|
|
98
98
|
idx = entries.find_index { |e| e.content.downcase == entry.content.downcase }
|
|
99
99
|
raise RobotLab::Error, "Cannot confirm: entry not found in domain '#{entry.domain}'" unless idx
|
|
100
|
+
|
|
100
101
|
entries[idx] = entry
|
|
101
102
|
save_domain(entry.domain, entries)
|
|
102
103
|
end
|
|
103
104
|
end
|
|
104
105
|
|
|
105
106
|
def domain_file(domain)
|
|
106
|
-
safe = domain.to_s.downcase.gsub(/[^a-z0-9]+/,
|
|
107
|
+
safe = domain.to_s.downcase.gsub(/[^a-z0-9]+/, '_').delete_prefix('_').delete_suffix('_')
|
|
107
108
|
File.join(@path, "#{safe}.yaml")
|
|
108
109
|
end
|
|
109
110
|
|
|
110
111
|
def with_domain_lock(domain, &block)
|
|
111
|
-
lock_path = domain_file(domain)
|
|
112
|
+
lock_path = "#{domain_file(domain)}.lock"
|
|
112
113
|
File.open(lock_path, File::RDWR | File::CREAT, 0o644) do |f|
|
|
113
114
|
f.flock(File::LOCK_EX)
|
|
114
115
|
block.call
|
data/lib/robot_lab/durable.rb
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
7
|
-
require_relative
|
|
3
|
+
require_relative 'durable/version'
|
|
4
|
+
require_relative 'durable/entry'
|
|
5
|
+
require_relative 'durable/store'
|
|
6
|
+
require_relative 'durable/reflector'
|
|
7
|
+
require_relative 'durable/learning'
|
|
8
8
|
|
|
9
9
|
# Minimal error stub so the storage layer works without robot_lab loaded.
|
|
10
10
|
# When robot_lab is present its own RobotLab::Error takes precedence.
|
|
@@ -15,10 +15,12 @@ end
|
|
|
15
15
|
# When robot_lab is loaded, register the knowledge tools and hook the
|
|
16
16
|
# Learning mixin into Robot so `learn: true` works in the constructor.
|
|
17
17
|
if defined?(RobotLab::Tool)
|
|
18
|
-
require_relative
|
|
19
|
-
require_relative
|
|
18
|
+
require_relative 'recall_knowledge'
|
|
19
|
+
require_relative 'record_knowledge'
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
if defined?(RobotLab::Robot)
|
|
23
|
-
|
|
22
|
+
RobotLab::Robot.include(RobotLab::Durable::Learning) if defined?(RobotLab::Robot)
|
|
23
|
+
|
|
24
|
+
if defined?(RobotLab) && RobotLab.respond_to?(:register_extension)
|
|
25
|
+
RobotLab.register_extension(:durable, RobotLab::Durable)
|
|
24
26
|
end
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module RobotLab
|
|
4
4
|
class RecallKnowledge < Tool
|
|
5
|
-
description
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
description 'Recall relevant knowledge from past sessions before making a decision. ' \
|
|
6
|
+
'Use this when uncertain whether to include or skip content, or when you want ' \
|
|
7
|
+
'to check if you have seen a similar situation before. ' \
|
|
8
|
+
'When in doubt and no relevant knowledge is found, skip the action.'
|
|
9
9
|
|
|
10
|
-
param :query, type:
|
|
11
|
-
param :domain, type:
|
|
10
|
+
param :query, type: 'string', desc: 'Natural language description of the decision you are about to make'
|
|
11
|
+
param :domain, type: 'string', desc: "Topic area to search (e.g. 'newsletter curation')", required: false
|
|
12
12
|
|
|
13
13
|
def execute(query:, domain: nil)
|
|
14
14
|
store = robot&.durable_store
|
|
15
|
-
return
|
|
15
|
+
return 'No durable store configured on this robot.' unless store
|
|
16
16
|
|
|
17
17
|
entries = store.recall(query: query, domain: domain, min_confidence: 0.0)
|
|
18
18
|
|
|
@@ -20,7 +20,7 @@ module RobotLab
|
|
|
20
20
|
"No relevant past knowledge found for: #{query}. When in doubt, skip."
|
|
21
21
|
else
|
|
22
22
|
lines = entries.map do |e|
|
|
23
|
-
"[#{e.category}/conf:#{format(
|
|
23
|
+
"[#{e.category}/conf:#{format('%.1f', e.confidence)}] #{e.content} — #{e.reasoning}"
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
"Relevant past knowledge:\n#{lines.join("\n")}"
|
|
@@ -2,28 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
module RobotLab
|
|
4
4
|
class RecordKnowledge < Tool
|
|
5
|
-
description
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
description 'Record a piece of knowledge learned during this session. ' \
|
|
6
|
+
'Use after a decision or discussion reveals something worth remembering: ' \
|
|
7
|
+
'a user preference, a reliable pattern, or a factual insight. ' \
|
|
8
|
+
'Recorded knowledge persists across future sessions.'
|
|
9
9
|
|
|
10
|
-
param :content, type:
|
|
11
|
-
param :reasoning, type:
|
|
12
|
-
|
|
13
|
-
param :
|
|
10
|
+
param :content, type: 'string', desc: 'The knowledge to record, in plain language (one clear statement)'
|
|
11
|
+
param :reasoning, type: 'string',
|
|
12
|
+
desc: 'Why this is worth remembering — the observation or discussion that led to it'
|
|
13
|
+
param :category, type: 'string', desc: 'One of: fact, preference, pattern, correction'
|
|
14
|
+
param :domain, type: 'string', desc: "Topic area this applies to (e.g. 'newsletter curation', 'ruby tooling')"
|
|
14
15
|
|
|
15
16
|
def execute(content:, reasoning:, category:, domain:)
|
|
16
17
|
store = robot&.durable_store
|
|
17
|
-
return
|
|
18
|
+
return 'No durable store configured on this robot.' unless store
|
|
18
19
|
|
|
19
20
|
now = Time.now.iso8601
|
|
20
21
|
entry = Durable::Entry.new(
|
|
21
22
|
content:,
|
|
22
23
|
reasoning:,
|
|
23
|
-
category:
|
|
24
|
+
category: category.to_sym,
|
|
24
25
|
domain:,
|
|
25
26
|
confidence: 0.1,
|
|
26
|
-
use_count:
|
|
27
|
+
use_count: 0,
|
|
27
28
|
created_at: now,
|
|
28
29
|
updated_at: now
|
|
29
30
|
)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: robot_lab-durable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -13,16 +13,16 @@ dependencies:
|
|
|
13
13
|
name: robot_lab
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
18
|
+
version: 0.2.0
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
|
-
- - "
|
|
23
|
+
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
25
|
+
version: 0.2.0
|
|
26
26
|
description: Provides RobotLab::Durable — a YAML-backed knowledge store that lets
|
|
27
27
|
robot_lab agents accumulate and recall observations across sessions. Includes Entry
|
|
28
28
|
(immutable value object with confidence scoring), Store (file-locked per-domain
|
|
@@ -36,10 +36,12 @@ extra_rdoc_files: []
|
|
|
36
36
|
files:
|
|
37
37
|
- ".envrc"
|
|
38
38
|
- ".github/workflows/deploy-github-pages.yml"
|
|
39
|
+
- ".rubocop.yml"
|
|
39
40
|
- CHANGELOG.md
|
|
40
41
|
- LICENSE.txt
|
|
41
42
|
- README.md
|
|
42
43
|
- Rakefile
|
|
44
|
+
- docs/document_store_backend_design.md
|
|
43
45
|
- docs/index.md
|
|
44
46
|
- docs/superpowers/plans/2026-05-06-durable-learning.md
|
|
45
47
|
- docs/superpowers/specs/2026-05-06-durable-learning-design.md
|