robot_lab-durable 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.envrc +1 -0
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +83 -0
- data/Rakefile +8 -0
- data/docs/index.md +62 -0
- data/docs/superpowers/plans/2026-05-06-durable-learning.md +1247 -0
- data/docs/superpowers/specs/2026-05-06-durable-learning-design.md +182 -0
- data/examples/33_stock_generator.rb +80 -0
- data/examples/33_stock_predictor.rb +304 -0
- data/lib/robot_lab/durable/entry.rb +49 -0
- data/lib/robot_lab/durable/learning.rb +39 -0
- data/lib/robot_lab/durable/reflector.rb +47 -0
- data/lib/robot_lab/durable/store.rb +119 -0
- data/lib/robot_lab/durable/version.rb +7 -0
- data/lib/robot_lab/durable.rb +24 -0
- data/lib/robot_lab/recall_knowledge.rb +30 -0
- data/lib/robot_lab/record_knowledge.rb +37 -0
- data/mkdocs.yml +116 -0
- metadata +82 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
module Durable
|
|
5
|
+
class Reflector
|
|
6
|
+
def initialize(store:, domain:)
|
|
7
|
+
@store = store
|
|
8
|
+
@domain = domain.to_s
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Examine plain-text learnings accumulated during a session and promote
|
|
12
|
+
# any that are not already represented in the store.
|
|
13
|
+
#
|
|
14
|
+
# @param learnings [Array<String>] robot.learnings from the completed session
|
|
15
|
+
def reflect(learnings)
|
|
16
|
+
Array(learnings).each do |text|
|
|
17
|
+
next if text.nil? || text.strip.empty?
|
|
18
|
+
|
|
19
|
+
text = text.strip
|
|
20
|
+
next if already_stored?(text)
|
|
21
|
+
|
|
22
|
+
now = Time.now.iso8601
|
|
23
|
+
@store.record(
|
|
24
|
+
Entry.new(
|
|
25
|
+
content: text,
|
|
26
|
+
reasoning: "Observed during session (auto-promoted by Reflector)",
|
|
27
|
+
category: :pattern,
|
|
28
|
+
domain: @domain,
|
|
29
|
+
confidence: 0.1,
|
|
30
|
+
use_count: 0,
|
|
31
|
+
created_at: now,
|
|
32
|
+
updated_at: now
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def already_stored?(text)
|
|
41
|
+
@store.recall(query: text, domain: @domain, min_confidence: 0.0).any? do |e|
|
|
42
|
+
e.content.downcase == text.downcase
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module RobotLab
|
|
7
|
+
module Durable
|
|
8
|
+
class Store
|
|
9
|
+
DEFAULT_PATH = File.join(Dir.home, ".robot_lab", "durable")
|
|
10
|
+
|
|
11
|
+
MIN_WORD_LENGTH = 3
|
|
12
|
+
|
|
13
|
+
def initialize(path: DEFAULT_PATH)
|
|
14
|
+
@path = path
|
|
15
|
+
FileUtils.mkdir_p(@path)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Return entries matching query keywords, sorted by descending confidence.
|
|
19
|
+
#
|
|
20
|
+
# @param query [String] natural-language search string
|
|
21
|
+
# @param domain [String, nil] restrict to one domain file; nil searches all
|
|
22
|
+
# @param min_confidence [Float] exclude entries below this threshold
|
|
23
|
+
# @return [Array<Entry>]
|
|
24
|
+
def recall(query:, domain: nil, min_confidence: 0.0)
|
|
25
|
+
entries = domain ? load_domain(domain) : load_all
|
|
26
|
+
words = tokenize(query)
|
|
27
|
+
|
|
28
|
+
entries
|
|
29
|
+
.select { |e| e.confidence >= min_confidence }
|
|
30
|
+
.select { |e| matches?(e, words) }
|
|
31
|
+
.sort_by { |e| -e.confidence }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Persist a new entry. If an entry with the same content already exists
|
|
35
|
+
# in the domain file, increment its confidence and use_count instead.
|
|
36
|
+
#
|
|
37
|
+
# @param entry [Entry]
|
|
38
|
+
# @return [Entry] the stored entry (may differ if an existing one was updated)
|
|
39
|
+
def record(entry)
|
|
40
|
+
with_domain_lock(entry.domain) do
|
|
41
|
+
entries = load_domain(entry.domain)
|
|
42
|
+
idx = entries.find_index { |e| e.content.downcase == entry.content.downcase }
|
|
43
|
+
|
|
44
|
+
if idx
|
|
45
|
+
entries[idx] = entries[idx].confirm
|
|
46
|
+
else
|
|
47
|
+
entries << entry
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
save_domain(entry.domain, entries)
|
|
51
|
+
entries[idx || -1]
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Increment confidence and use_count on a stored entry.
|
|
56
|
+
#
|
|
57
|
+
# @param entry [Entry]
|
|
58
|
+
# @return [Entry] the updated entry
|
|
59
|
+
def confirm(entry)
|
|
60
|
+
updated = entry.confirm
|
|
61
|
+
record_exact(updated)
|
|
62
|
+
updated
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def matches?(entry, words)
|
|
68
|
+
text = "#{entry.content} #{entry.domain}".downcase
|
|
69
|
+
words.any? { |w| text.include?(w) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def tokenize(str)
|
|
73
|
+
str.downcase.split(/\s+/).reject { |w| w.length < MIN_WORD_LENGTH }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def load_domain(domain)
|
|
77
|
+
file = domain_file(domain)
|
|
78
|
+
return [] unless File.exist?(file)
|
|
79
|
+
|
|
80
|
+
raw = Array(YAML.safe_load(File.read(file)) || [])
|
|
81
|
+
raw.map { |h| Entry.from_h(h) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def load_all
|
|
85
|
+
Dir.glob(File.join(@path, "*.yaml")).flat_map do |file|
|
|
86
|
+
raw = Array(YAML.safe_load(File.read(file)) || [])
|
|
87
|
+
raw.map { |h| Entry.from_h(h) }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def save_domain(domain, entries)
|
|
92
|
+
File.write(domain_file(domain), YAML.dump(entries.map(&:to_h)))
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def record_exact(entry)
|
|
96
|
+
with_domain_lock(entry.domain) do
|
|
97
|
+
entries = load_domain(entry.domain)
|
|
98
|
+
idx = entries.find_index { |e| e.content.downcase == entry.content.downcase }
|
|
99
|
+
raise RobotLab::Error, "Cannot confirm: entry not found in domain '#{entry.domain}'" unless idx
|
|
100
|
+
entries[idx] = entry
|
|
101
|
+
save_domain(entry.domain, entries)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def domain_file(domain)
|
|
106
|
+
safe = domain.to_s.downcase.gsub(/[^a-z0-9]+/, "_").delete_prefix("_").delete_suffix("_")
|
|
107
|
+
File.join(@path, "#{safe}.yaml")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def with_domain_lock(domain, &block)
|
|
111
|
+
lock_path = domain_file(domain) + ".lock"
|
|
112
|
+
File.open(lock_path, File::RDWR | File::CREAT, 0o644) do |f|
|
|
113
|
+
f.flock(File::LOCK_EX)
|
|
114
|
+
block.call
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
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
|
+
|
|
9
|
+
# Minimal error stub so the storage layer works without robot_lab loaded.
|
|
10
|
+
# When robot_lab is present its own RobotLab::Error takes precedence.
|
|
11
|
+
module RobotLab
|
|
12
|
+
Error = StandardError unless defined?(Error)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# When robot_lab is loaded, register the knowledge tools and hook the
|
|
16
|
+
# Learning mixin into Robot so `learn: true` works in the constructor.
|
|
17
|
+
if defined?(RobotLab::Tool)
|
|
18
|
+
require_relative "recall_knowledge"
|
|
19
|
+
require_relative "record_knowledge"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
if defined?(RobotLab::Robot)
|
|
23
|
+
RobotLab::Robot.include(RobotLab::Durable::Learning)
|
|
24
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
class RecallKnowledge < Tool
|
|
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
|
+
|
|
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
|
+
|
|
13
|
+
def execute(query:, domain: nil)
|
|
14
|
+
store = robot&.durable_store
|
|
15
|
+
return "No durable store configured on this robot." unless store
|
|
16
|
+
|
|
17
|
+
entries = store.recall(query: query, domain: domain, min_confidence: 0.0)
|
|
18
|
+
|
|
19
|
+
if entries.empty?
|
|
20
|
+
"No relevant past knowledge found for: #{query}. When in doubt, skip."
|
|
21
|
+
else
|
|
22
|
+
lines = entries.map do |e|
|
|
23
|
+
"[#{e.category}/conf:#{format("%.1f", e.confidence)}] #{e.content} — #{e.reasoning}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
"Relevant past knowledge:\n#{lines.join("\n")}"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RobotLab
|
|
4
|
+
class RecordKnowledge < Tool
|
|
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
|
+
|
|
10
|
+
param :content, type: "string", desc: "The knowledge to record, in plain language (one clear statement)"
|
|
11
|
+
param :reasoning, type: "string", desc: "Why this is worth remembering — the observation or discussion that led to it"
|
|
12
|
+
param :category, type: "string", desc: "One of: fact, preference, pattern, correction"
|
|
13
|
+
param :domain, type: "string", desc: "Topic area this applies to (e.g. 'newsletter curation', 'ruby tooling')"
|
|
14
|
+
|
|
15
|
+
def execute(content:, reasoning:, category:, domain:)
|
|
16
|
+
store = robot&.durable_store
|
|
17
|
+
return "No durable store configured on this robot." unless store
|
|
18
|
+
|
|
19
|
+
now = Time.now.iso8601
|
|
20
|
+
entry = Durable::Entry.new(
|
|
21
|
+
content:,
|
|
22
|
+
reasoning:,
|
|
23
|
+
category: category.to_sym,
|
|
24
|
+
domain:,
|
|
25
|
+
confidence: 0.1,
|
|
26
|
+
use_count: 0,
|
|
27
|
+
created_at: now,
|
|
28
|
+
updated_at: now
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
store.record(entry)
|
|
32
|
+
robot.learn("#{content} (#{domain})")
|
|
33
|
+
|
|
34
|
+
"Recorded: #{content}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
data/mkdocs.yml
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
site_name: robot_lab-durable
|
|
2
|
+
site_description: Cross-session durable learning for the RobotLab LLM agent framework
|
|
3
|
+
site_author: Dewayne VanHoozer
|
|
4
|
+
site_url: https://madbomber.github.io/robot_lab-durable
|
|
5
|
+
copyright: Copyright © 2025 Dewayne VanHoozer
|
|
6
|
+
|
|
7
|
+
repo_name: MadBomber/robot_lab-durable
|
|
8
|
+
repo_url: https://github.com/MadBomber/robot_lab-durable
|
|
9
|
+
edit_uri: edit/main/docs/
|
|
10
|
+
|
|
11
|
+
theme:
|
|
12
|
+
name: material
|
|
13
|
+
|
|
14
|
+
palette:
|
|
15
|
+
- scheme: default
|
|
16
|
+
primary: green
|
|
17
|
+
accent: amber
|
|
18
|
+
toggle:
|
|
19
|
+
icon: material/brightness-7
|
|
20
|
+
name: Switch to dark mode
|
|
21
|
+
|
|
22
|
+
- scheme: slate
|
|
23
|
+
primary: green
|
|
24
|
+
accent: amber
|
|
25
|
+
toggle:
|
|
26
|
+
icon: material/brightness-4
|
|
27
|
+
name: Switch to light mode
|
|
28
|
+
|
|
29
|
+
font:
|
|
30
|
+
text: Roboto
|
|
31
|
+
code: Roboto Mono
|
|
32
|
+
|
|
33
|
+
icon:
|
|
34
|
+
repo: fontawesome/brands/github
|
|
35
|
+
logo: material/brain
|
|
36
|
+
|
|
37
|
+
features:
|
|
38
|
+
- navigation.instant
|
|
39
|
+
- navigation.tracking
|
|
40
|
+
- navigation.tabs
|
|
41
|
+
- navigation.tabs.sticky
|
|
42
|
+
- navigation.path
|
|
43
|
+
- navigation.indexes
|
|
44
|
+
- navigation.top
|
|
45
|
+
- navigation.footer
|
|
46
|
+
- toc.follow
|
|
47
|
+
- search.suggest
|
|
48
|
+
- search.highlight
|
|
49
|
+
- search.share
|
|
50
|
+
- header.autohide
|
|
51
|
+
- content.code.copy
|
|
52
|
+
- content.code.annotate
|
|
53
|
+
- content.tabs.link
|
|
54
|
+
- content.tooltips
|
|
55
|
+
- content.action.edit
|
|
56
|
+
- content.action.view
|
|
57
|
+
|
|
58
|
+
plugins:
|
|
59
|
+
- search:
|
|
60
|
+
separator: '[\s\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])'
|
|
61
|
+
|
|
62
|
+
markdown_extensions:
|
|
63
|
+
- abbr
|
|
64
|
+
- admonition
|
|
65
|
+
- attr_list
|
|
66
|
+
- def_list
|
|
67
|
+
- footnotes
|
|
68
|
+
- md_in_html
|
|
69
|
+
- tables
|
|
70
|
+
- toc:
|
|
71
|
+
permalink: true
|
|
72
|
+
title: On this page
|
|
73
|
+
- pymdownx.betterem:
|
|
74
|
+
smart_enable: all
|
|
75
|
+
- pymdownx.caret
|
|
76
|
+
- pymdownx.details
|
|
77
|
+
- pymdownx.emoji:
|
|
78
|
+
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
|
79
|
+
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
|
80
|
+
- pymdownx.highlight:
|
|
81
|
+
anchor_linenums: true
|
|
82
|
+
line_spans: __span
|
|
83
|
+
pygments_lang_class: true
|
|
84
|
+
- pymdownx.inlinehilite
|
|
85
|
+
- pymdownx.magiclink:
|
|
86
|
+
repo_url_shorthand: true
|
|
87
|
+
user: MadBomber
|
|
88
|
+
repo: robot_lab-durable
|
|
89
|
+
normalize_issue_symbols: true
|
|
90
|
+
- pymdownx.mark
|
|
91
|
+
- pymdownx.smartsymbols
|
|
92
|
+
- pymdownx.superfences:
|
|
93
|
+
custom_fences:
|
|
94
|
+
- name: mermaid
|
|
95
|
+
class: mermaid
|
|
96
|
+
format: !!python/name:pymdownx.superfences.fence_code_format
|
|
97
|
+
- pymdownx.tabbed:
|
|
98
|
+
alternate_style: true
|
|
99
|
+
- pymdownx.tasklist:
|
|
100
|
+
custom_checkbox: true
|
|
101
|
+
- pymdownx.tilde
|
|
102
|
+
|
|
103
|
+
extra:
|
|
104
|
+
social:
|
|
105
|
+
- icon: fontawesome/brands/github
|
|
106
|
+
link: https://github.com/MadBomber/robot_lab-durable
|
|
107
|
+
name: robot_lab-durable on GitHub
|
|
108
|
+
- icon: fontawesome/solid/gem
|
|
109
|
+
link: https://rubygems.org/gems/robot_lab-durable
|
|
110
|
+
name: robot_lab-durable on RubyGems
|
|
111
|
+
|
|
112
|
+
nav:
|
|
113
|
+
- Home: index.md
|
|
114
|
+
- Design & Planning:
|
|
115
|
+
- Implementation Plan: superpowers/plans/2026-05-06-durable-learning.md
|
|
116
|
+
- Design Spec: superpowers/specs/2026-05-06-durable-learning-design.md
|
metadata
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: robot_lab-durable
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Dewayne VanHoozer
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: robot_lab
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
description: Provides RobotLab::Durable — a YAML-backed knowledge store that lets
|
|
27
|
+
robot_lab agents accumulate and recall observations across sessions. Includes Entry
|
|
28
|
+
(immutable value object with confidence scoring), Store (file-locked per-domain
|
|
29
|
+
persistence), Reflector (end-of-session promoter), and the Learning mixin with RecallKnowledge/RecordKnowledge
|
|
30
|
+
tools that integrate directly into Robot when robot_lab is present.
|
|
31
|
+
email:
|
|
32
|
+
- dvanhoozer@gmail.com
|
|
33
|
+
executables: []
|
|
34
|
+
extensions: []
|
|
35
|
+
extra_rdoc_files: []
|
|
36
|
+
files:
|
|
37
|
+
- ".envrc"
|
|
38
|
+
- ".github/workflows/deploy-github-pages.yml"
|
|
39
|
+
- CHANGELOG.md
|
|
40
|
+
- LICENSE.txt
|
|
41
|
+
- README.md
|
|
42
|
+
- Rakefile
|
|
43
|
+
- docs/index.md
|
|
44
|
+
- docs/superpowers/plans/2026-05-06-durable-learning.md
|
|
45
|
+
- docs/superpowers/specs/2026-05-06-durable-learning-design.md
|
|
46
|
+
- examples/33_stock_generator.rb
|
|
47
|
+
- examples/33_stock_predictor.rb
|
|
48
|
+
- lib/robot_lab/durable.rb
|
|
49
|
+
- lib/robot_lab/durable/entry.rb
|
|
50
|
+
- lib/robot_lab/durable/learning.rb
|
|
51
|
+
- lib/robot_lab/durable/reflector.rb
|
|
52
|
+
- lib/robot_lab/durable/store.rb
|
|
53
|
+
- lib/robot_lab/durable/version.rb
|
|
54
|
+
- lib/robot_lab/recall_knowledge.rb
|
|
55
|
+
- lib/robot_lab/record_knowledge.rb
|
|
56
|
+
- mkdocs.yml
|
|
57
|
+
homepage: https://github.com/MadBomber/robot_lab-durable
|
|
58
|
+
licenses:
|
|
59
|
+
- MIT
|
|
60
|
+
metadata:
|
|
61
|
+
homepage_uri: https://github.com/MadBomber/robot_lab-durable
|
|
62
|
+
source_code_uri: https://github.com/MadBomber/robot_lab-durable
|
|
63
|
+
changelog_uri: https://github.com/MadBomber/robot_lab-durable/blob/main/CHANGELOG.md
|
|
64
|
+
rubygems_mfa_required: 'true'
|
|
65
|
+
rdoc_options: []
|
|
66
|
+
require_paths:
|
|
67
|
+
- lib
|
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: 3.2.0
|
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '0'
|
|
78
|
+
requirements: []
|
|
79
|
+
rubygems_version: 4.0.11
|
|
80
|
+
specification_version: 4
|
|
81
|
+
summary: Cross-session durable learning for RobotLab agents
|
|
82
|
+
test_files: []
|