robot_lab 0.2.1 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +1 -0
  3. data/.loki +60 -0
  4. data/.quality/reek_baseline.txt +43 -0
  5. data/.rubocop.yml +5 -167
  6. data/CHANGELOG.md +54 -0
  7. data/README.md +61 -0
  8. data/Rakefile +28 -12
  9. data/docs/api/core/index.md +1 -0
  10. data/docs/api/core/network.md +31 -1
  11. data/docs/api/core/robot.md +91 -5
  12. data/docs/api/errors.md +47 -2
  13. data/docs/api/index.md +3 -0
  14. data/docs/architecture/core-concepts.md +40 -0
  15. data/docs/architecture/network-orchestration.md +8 -0
  16. data/docs/architecture/robot-execution.md +1 -1
  17. data/docs/getting-started/configuration.md +20 -1
  18. data/docs/guides/building-robots.md +39 -1
  19. data/docs/guides/creating-networks.md +6 -1
  20. data/docs/guides/hooks.md +997 -0
  21. data/docs/guides/index.md +5 -0
  22. data/docs/guides/knowledge.md +2 -2
  23. data/docs/guides/observability.md +74 -7
  24. data/docs/guides/using-tools.md +69 -0
  25. data/docs/index.md +1 -1
  26. data/examples/03_network.rb +11 -2
  27. data/examples/34_agentskills.rb +0 -0
  28. data/examples/35_hooks.rb +256 -0
  29. data/examples/README.md +7 -0
  30. data/examples/common.rb +7 -3
  31. data/examples/xyzzy.rb +90 -0
  32. data/lib/robot_lab/agent_skill.rb +5 -4
  33. data/lib/robot_lab/budget/ledger.rb +98 -0
  34. data/lib/robot_lab/capabilities.rb +84 -0
  35. data/lib/robot_lab/config/defaults.yml +10 -0
  36. data/lib/robot_lab/config.rb +4 -4
  37. data/lib/robot_lab/error.rb +34 -4
  38. data/lib/robot_lab/errors.rb +45 -0
  39. data/lib/robot_lab/hook.rb +79 -0
  40. data/lib/robot_lab/hook_context.rb +194 -0
  41. data/lib/robot_lab/hook_registry.rb +55 -0
  42. data/lib/robot_lab/hooks.rb +87 -0
  43. data/lib/robot_lab/mcp/connection_poller.rb +2 -2
  44. data/lib/robot_lab/narrator.rb +87 -0
  45. data/lib/robot_lab/network.rb +77 -20
  46. data/lib/robot_lab/robot/budget.rb +89 -0
  47. data/lib/robot_lab/robot/bus_messaging.rb +76 -12
  48. data/lib/robot_lab/robot/hooking.rb +56 -0
  49. data/lib/robot_lab/robot/mcp_management.rb +8 -2
  50. data/lib/robot_lab/robot/template_rendering.rb +2 -2
  51. data/lib/robot_lab/robot.rb +211 -88
  52. data/lib/robot_lab/run_config.rb +24 -5
  53. data/lib/robot_lab/runnable.rb +51 -0
  54. data/lib/robot_lab/sandbox/null.rb +13 -0
  55. data/lib/robot_lab/sandbox/seatbelt.rb +104 -0
  56. data/lib/robot_lab/sandbox.rb +52 -0
  57. data/lib/robot_lab/script_tool.rb +60 -2
  58. data/lib/robot_lab/task.rb +26 -20
  59. data/lib/robot_lab/tool.rb +52 -11
  60. data/lib/robot_lab/version.rb +1 -1
  61. data/lib/robot_lab.rb +49 -0
  62. data/mkdocs.yml +1 -0
  63. metadata +36 -99
  64. data/examples/temp.md +0 -51
  65. data/site/404.html +0 -2300
  66. data/site/api/core/index.html +0 -2706
  67. data/site/api/core/memory/index.html +0 -3793
  68. data/site/api/core/network/index.html +0 -3500
  69. data/site/api/core/robot/index.html +0 -4566
  70. data/site/api/core/state/index.html +0 -3390
  71. data/site/api/core/tool/index.html +0 -3843
  72. data/site/api/index.html +0 -2635
  73. data/site/api/mcp/client/index.html +0 -3435
  74. data/site/api/mcp/index.html +0 -2783
  75. data/site/api/mcp/server/index.html +0 -3252
  76. data/site/api/mcp/transports/index.html +0 -3352
  77. data/site/api/messages/index.html +0 -2641
  78. data/site/api/messages/text-message/index.html +0 -3087
  79. data/site/api/messages/tool-call-message/index.html +0 -3159
  80. data/site/api/messages/tool-result-message/index.html +0 -3252
  81. data/site/api/messages/user-message/index.html +0 -3212
  82. data/site/api/streaming/context/index.html +0 -3282
  83. data/site/api/streaming/events/index.html +0 -3347
  84. data/site/api/streaming/index.html +0 -2738
  85. data/site/architecture/core-concepts/index.html +0 -3757
  86. data/site/architecture/index.html +0 -2797
  87. data/site/architecture/message-flow/index.html +0 -3238
  88. data/site/architecture/network-orchestration/index.html +0 -3433
  89. data/site/architecture/robot-execution/index.html +0 -3140
  90. data/site/architecture/state-management/index.html +0 -3498
  91. data/site/assets/css/custom.css +0 -56
  92. data/site/assets/images/favicon.png +0 -0
  93. data/site/assets/images/robot_lab.jpg +0 -0
  94. data/site/assets/javascripts/bundle.79ae519e.min.js +0 -16
  95. data/site/assets/javascripts/bundle.79ae519e.min.js.map +0 -7
  96. data/site/assets/javascripts/lunr/min/lunr.ar.min.js +0 -1
  97. data/site/assets/javascripts/lunr/min/lunr.da.min.js +0 -18
  98. data/site/assets/javascripts/lunr/min/lunr.de.min.js +0 -18
  99. data/site/assets/javascripts/lunr/min/lunr.du.min.js +0 -18
  100. data/site/assets/javascripts/lunr/min/lunr.el.min.js +0 -1
  101. data/site/assets/javascripts/lunr/min/lunr.es.min.js +0 -18
  102. data/site/assets/javascripts/lunr/min/lunr.fi.min.js +0 -18
  103. data/site/assets/javascripts/lunr/min/lunr.fr.min.js +0 -18
  104. data/site/assets/javascripts/lunr/min/lunr.he.min.js +0 -1
  105. data/site/assets/javascripts/lunr/min/lunr.hi.min.js +0 -1
  106. data/site/assets/javascripts/lunr/min/lunr.hu.min.js +0 -18
  107. data/site/assets/javascripts/lunr/min/lunr.hy.min.js +0 -1
  108. data/site/assets/javascripts/lunr/min/lunr.it.min.js +0 -18
  109. data/site/assets/javascripts/lunr/min/lunr.ja.min.js +0 -1
  110. data/site/assets/javascripts/lunr/min/lunr.jp.min.js +0 -1
  111. data/site/assets/javascripts/lunr/min/lunr.kn.min.js +0 -1
  112. data/site/assets/javascripts/lunr/min/lunr.ko.min.js +0 -1
  113. data/site/assets/javascripts/lunr/min/lunr.multi.min.js +0 -1
  114. data/site/assets/javascripts/lunr/min/lunr.nl.min.js +0 -18
  115. data/site/assets/javascripts/lunr/min/lunr.no.min.js +0 -18
  116. data/site/assets/javascripts/lunr/min/lunr.pt.min.js +0 -18
  117. data/site/assets/javascripts/lunr/min/lunr.ro.min.js +0 -18
  118. data/site/assets/javascripts/lunr/min/lunr.ru.min.js +0 -18
  119. data/site/assets/javascripts/lunr/min/lunr.sa.min.js +0 -1
  120. data/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +0 -1
  121. data/site/assets/javascripts/lunr/min/lunr.sv.min.js +0 -18
  122. data/site/assets/javascripts/lunr/min/lunr.ta.min.js +0 -1
  123. data/site/assets/javascripts/lunr/min/lunr.te.min.js +0 -1
  124. data/site/assets/javascripts/lunr/min/lunr.th.min.js +0 -1
  125. data/site/assets/javascripts/lunr/min/lunr.tr.min.js +0 -18
  126. data/site/assets/javascripts/lunr/min/lunr.vi.min.js +0 -1
  127. data/site/assets/javascripts/lunr/min/lunr.zh.min.js +0 -1
  128. data/site/assets/javascripts/lunr/tinyseg.js +0 -206
  129. data/site/assets/javascripts/lunr/wordcut.js +0 -6708
  130. data/site/assets/javascripts/workers/search.2c215733.min.js +0 -42
  131. data/site/assets/javascripts/workers/search.2c215733.min.js.map +0 -7
  132. data/site/assets/stylesheets/main.484c7ddc.min.css +0 -1
  133. data/site/assets/stylesheets/main.484c7ddc.min.css.map +0 -1
  134. data/site/assets/stylesheets/palette.ab4e12ef.min.css +0 -1
  135. data/site/assets/stylesheets/palette.ab4e12ef.min.css.map +0 -1
  136. data/site/concepts/index.html +0 -3455
  137. data/site/examples/basic-chat/index.html +0 -2880
  138. data/site/examples/index.html +0 -2907
  139. data/site/examples/mcp-server/index.html +0 -3018
  140. data/site/examples/multi-robot-network/index.html +0 -3131
  141. data/site/examples/rails-application/index.html +0 -3329
  142. data/site/examples/tool-usage/index.html +0 -3085
  143. data/site/getting-started/configuration/index.html +0 -3745
  144. data/site/getting-started/index.html +0 -2572
  145. data/site/getting-started/installation/index.html +0 -2981
  146. data/site/getting-started/quick-start/index.html +0 -2942
  147. data/site/guides/building-robots/index.html +0 -4290
  148. data/site/guides/creating-networks/index.html +0 -3858
  149. data/site/guides/index.html +0 -2586
  150. data/site/guides/mcp-integration/index.html +0 -3581
  151. data/site/guides/memory/index.html +0 -3586
  152. data/site/guides/rails-integration/index.html +0 -4019
  153. data/site/guides/streaming/index.html +0 -3157
  154. data/site/guides/using-tools/index.html +0 -3802
  155. data/site/index.html +0 -2671
  156. data/site/search/search_index.json +0 -1
  157. data/site/sitemap.xml +0 -183
  158. data/site/sitemap.xml.gz +0 -0
  159. data/site/tags.json +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '020282572c5c4eca7e0c44d60920752df53043df99aa54a2018b68e049404945'
4
- data.tar.gz: 2f0d0e92ff91a8eb24ff73065e1b91091c57cb6abcf3a0c3349c2370ab877973
3
+ metadata.gz: d401853bc5fff2646793cfff4fbda7082b18b0e07b833b3f4de6545fca574125
4
+ data.tar.gz: f9964095406885bed329d8f2e7189cbb582e1c4665e3d53781b83371ee9016c7
5
5
  SHA512:
6
- metadata.gz: 7d99549cbb526071e48e2d66e9771553491404ec437f21465190517fc4451418a48024cd5e1fc8482ae96ea061492fb632f8f264048d8941e20090faa1213d22
7
- data.tar.gz: 0fe87b21e72c03a65e3809dfab01d3d6735919101ab8f2f8d9cd9d64a52394c06fc12f9fc9b648cb9350568c56ac8da111d8a6db9ba841738cbb72ce682708d6
6
+ metadata.gz: dd29d885c9ea4e0b91bffe49b757808213e8011302fac48b4632c37d0cc95315a559f80240fabca92c0a309fead3f1f72b1da3ac7eccdb440059451fc53c5a0e
7
+ data.tar.gz: 518c45909e25437da64e16ad69a5bfe5f361ddd602dc64c0ad475acde1c0b7946a0773fa36960a0baea3294ae7a7cef1835b6d08be2034e3a4a905fa74715eb2
data/.envrc CHANGED
@@ -1 +1,2 @@
1
1
  export RR=`pwd`
2
+ export BUNDLE_GEMFILE=Gemfile
data/.loki ADDED
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+ # robot_lab core — development tasks.
3
+
4
+ import_up "repo_dev.loki"
5
+
6
+ class Tasks
7
+ @@gem_name ||= "robot_lab".freeze
8
+
9
+ header "robot_lab v#{gem_version} — core multi-robot LLM framework"
10
+
11
+ desc "Run integration tests only"
12
+ def integration
13
+ sh "bundle exec rake integration"
14
+ end
15
+
16
+ desc "Run a single test file (e.g. asgard test_file test/robot_test.rb)"
17
+ def test_file(path)
18
+ sh "ruby -Ilib:test \"#{path}\""
19
+ end
20
+
21
+ class DocsCommands < Tasks
22
+ desc "Build YARD + MkDocs documentation"
23
+ def build
24
+ sh "bundle exec rake docs:build"
25
+ end
26
+
27
+ desc "Serve MkDocs documentation at http://localhost:8000"
28
+ def serve
29
+ sh "mkdocs serve"
30
+ end
31
+
32
+ desc "Serve YARD documentation locally"
33
+ def yard
34
+ sh "yard server --reload"
35
+ end
36
+ end
37
+
38
+ class ExamplesCommands < Tasks
39
+ desc "Run all examples (skips standalone apps and external-service examples)"
40
+ def all
41
+ sh "bundle exec rake examples:all"
42
+ end
43
+
44
+ desc "exec N", "Run a specific example by number (e.g. asgard examples exec 3)"
45
+ def exec(num)
46
+ sh "bundle exec rake examples:run[#{num}]"
47
+ end
48
+
49
+ desc "Start the Rails demo app (example 18)"
50
+ def rails
51
+ sh "bundle exec rake examples:rails"
52
+ end
53
+ end
54
+
55
+ desc "docs SUBCOMMAND", "Documentation tasks (build, serve, yard)"
56
+ subcommand "docs", DocsCommands
57
+
58
+ desc "examples SUBCOMMAND", "Example tasks (all, exec N, rails)"
59
+ subcommand "examples", ExamplesCommands
60
+ end
@@ -0,0 +1,43 @@
1
+ 4 lib/robot_lab.rb
2
+ 1 lib/robot_lab/agent_skill_catalog.rb
3
+ 2 lib/robot_lab/ask_user.rb
4
+ 2 lib/robot_lab/bus_poller.rb
5
+ 4 lib/robot_lab/capabilities.rb
6
+ 8 lib/robot_lab/config.rb
7
+ 3 lib/robot_lab/doom_loop_detector.rb
8
+ 6 lib/robot_lab/history_compressor.rb
9
+ 1 lib/robot_lab/hook.rb
10
+ 4 lib/robot_lab/hook_context.rb
11
+ 1 lib/robot_lab/hook_registry.rb
12
+ 4 lib/robot_lab/hooks.rb
13
+ 3 lib/robot_lab/mcp/client.rb
14
+ 6 lib/robot_lab/mcp/connection_poller.rb
15
+ 1 lib/robot_lab/mcp/server_discovery.rb
16
+ 3 lib/robot_lab/mcp/transports/sse.rb
17
+ 6 lib/robot_lab/mcp/transports/stdio.rb
18
+ 4 lib/robot_lab/mcp/transports/streamable_http.rb
19
+ 3 lib/robot_lab/mcp/transports/websocket.rb
20
+ 16 lib/robot_lab/memory.rb
21
+ 1 lib/robot_lab/memory_change.rb
22
+ 3 lib/robot_lab/message.rb
23
+ 8 lib/robot_lab/network.rb
24
+ 70 lib/robot_lab/robot.rb
25
+ 2 lib/robot_lab/robot/agent_skill_matching.rb
26
+ 4 lib/robot_lab/robot/bus_messaging.rb
27
+ 2 lib/robot_lab/robot/history_search.rb
28
+ 2 lib/robot_lab/robot/hooking.rb
29
+ 9 lib/robot_lab/robot/mcp_management.rb
30
+ 8 lib/robot_lab/robot/template_rendering.rb
31
+ 4 lib/robot_lab/robot_result.rb
32
+ 5 lib/robot_lab/run_config.rb
33
+ 1 lib/robot_lab/sandbox.rb
34
+ 1 lib/robot_lab/sandbox/seatbelt.rb
35
+ 3 lib/robot_lab/script_tool.rb
36
+ 1 lib/robot_lab/state_proxy.rb
37
+ 1 lib/robot_lab/streaming/context.rb
38
+ 1 lib/robot_lab/streaming/events.rb
39
+ 2 lib/robot_lab/task.rb
40
+ 6 lib/robot_lab/tool.rb
41
+ 2 lib/robot_lab/user_message.rb
42
+ 2 lib/robot_lab/utils.rb
43
+ 2 lib/robot_lab/waiter.rb
data/.rubocop.yml CHANGED
@@ -1,172 +1,10 @@
1
+ inherit_from: ../.rubocop-base.yml
2
+
3
+ # Core-only overrides. Exclude patterns here are relative to this repo and
4
+ # merge with the shared base's excludes.
1
5
  AllCops:
2
- NewCops: enable
3
- SuggestExtensions: false
4
- TargetRubyVersion: 4.0
5
6
  Exclude:
6
- - 'examples/**/*'
7
- - 'vendor/**/*'
8
- - 'dead_code/**/*'
9
7
  - '_notes/**/*'
10
8
 
11
- # ── Style: disabled cops ───────────────────────────────────────────────────
12
- Style/StringLiterals:
13
- Enabled: false
14
-
15
- Style/StringLiteralsInInterpolation:
16
- Enabled: false
17
-
18
- Style/Documentation:
19
- Enabled: false
20
-
21
- # Ruby 4.0 freezes string literals by default
22
- Style/FrozenStringLiteralComment:
23
- Enabled: false
24
-
25
- Style/IfUnlessModifier:
26
- Enabled: false
27
-
28
- Style/RescueModifier:
29
- Enabled: false
30
-
31
- Style/TrivialAccessors:
32
- Enabled: false
33
-
34
- Style/MultilineTernaryOperator:
35
- Enabled: false
36
-
37
- Style/SafeNavigation:
38
- Enabled: false
39
-
40
- Style/EmptyClassDefinition:
41
- Enabled: false
42
-
43
- Style/ClassAndModuleChildren:
44
- Enabled: false
45
-
46
- Style/RescueStandardError:
47
- Enabled: false
48
-
49
- Style/OneClassPerFile:
50
- Enabled: false
51
-
52
- # Both % and format/sprintf are acceptable
53
- Style/FormatString:
54
- Enabled: false
55
-
56
- # String concatenation and interpolation are both acceptable
57
- Style/StringConcatenation:
58
- Enabled: false
59
-
60
- # ── Layout ─────────────────────────────────────────────────────────────────
61
- Layout/LineLength:
62
- Max: 140
63
-
64
- Layout/ExtraSpacing:
65
- Enabled: false
66
-
67
- Layout/HashAlignment:
68
- Enabled: false
69
-
70
- Layout/FirstHashElementIndentation:
71
- Enabled: false
72
-
73
- Layout/EmptyLineAfterGuardClause:
74
- Enabled: false
75
-
76
- # ── Naming ─────────────────────────────────────────────────────────────────
77
- # Single-char params (c, e, n) are acceptable throughout
78
- Naming/MethodParameterName:
79
- Enabled: false
80
-
81
- Naming/VariableNumber:
82
- Exclude:
83
- - 'test/**/*'
84
-
85
- Naming/RescuedExceptionsVariableName:
86
- Enabled: false
87
-
88
- # set_results and similar explicit setters are clear and conventional
89
- Naming/AccessorMethodName:
90
- Enabled: false
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
- Lint/EmptyBlock:
103
- Exclude:
104
- - 'test/**/*'
105
-
106
- # Library and framework methods commonly accept args for API/documentation purposes
107
- Lint/UnusedMethodArgument:
108
- Enabled: false
109
-
110
- Lint/ConstantDefinitionInBlock:
111
- Exclude:
112
- - 'Rakefile'
113
- - 'test/**/*'
114
-
115
- # ── Gemspec ────────────────────────────────────────────────────────────────
116
- Gemspec/DevelopmentDependencies:
117
- EnforcedStyle: Gemfile
118
-
119
- Gemspec/RequiredRubyVersion:
120
- Enabled: false
121
-
122
- Gemspec/OrderedDependencies:
123
- Enabled: false
124
-
125
- # ── Metrics ────────────────────────────────────────────────────────────────
126
- # Framework-level code (routers, parsers, orchestrators) is inherently complex.
127
- # Flog is the primary complexity gate — these RuboCop thresholds catch only
128
- # egregious outliers without false-positiving every dispatch method.
129
-
130
- Metrics/MethodLength:
131
- Max: 35
132
- CountAsOne:
133
- - heredoc
134
- - array
135
- - hash
136
- Exclude:
137
- - 'test/**/*'
138
-
139
- Metrics/AbcSize:
140
- Max: 40
141
- Exclude:
142
- - 'test/**/*'
143
-
144
9
  Metrics/ClassLength:
145
- Max: 600
146
- Exclude:
147
- - 'test/**/*'
148
-
149
- Metrics/ModuleLength:
150
- Max: 200
151
- Exclude:
152
- - 'test/**/*'
153
-
154
- Metrics/CyclomaticComplexity:
155
- Max: 20
156
- Exclude:
157
- - 'test/**/*'
158
-
159
- Metrics/PerceivedComplexity:
160
- Max: 20
161
- Exclude:
162
- - 'test/**/*'
163
-
164
- # Long method signatures with keyword args are a Ruby framework idiom
165
- Metrics/ParameterLists:
166
- Enabled: false
167
-
168
- Metrics/BlockLength:
169
- Exclude:
170
- - 'Rakefile'
171
- - '*.gemspec'
172
- - 'test/**/*'
10
+ Max: 650 # Robot is the central class; a future refactor could split it further
data/CHANGELOG.md CHANGED
@@ -8,6 +8,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ### Added
12
+ - `BusMessaging#respond_to_tasks` and `#serve` — a member auto-answers inbound
13
+ (non-reply) bus tasks and replies to the sender. `serve` runs each task through
14
+ `#run`; `respond_to_tasks { |m| ... }` is the generic form. Makes a Robot a
15
+ first-class bus responder (symmetric with how a Cyborg answers its human)
16
+ without hand-wiring `on_message`.
17
+ - `.loki` Asgard task file: `test`, `rubocop`, `rubocop_fix`, `flog`, `flay`, `quality`, `build`, `install`, `release`, `integration`, `docs`, and `examples` tasks via the Asgard task runner
18
+
19
+ ### Fixed
20
+ - `BusMessaging` now synchronizes its message counter and outbox with a mutex, so
21
+ concurrent sends and poller-thread reply correlation can't clobber each other.
22
+ - `flay_check` Rake task: structural code duplication gate (mass threshold 50); integrated into the `quality` Rake task
23
+ - `flay` gem added to development dependencies
24
+ - `test_output.txt`, `flay_output.txt`, `flog_output.txt`, and `rubocop_output.txt` added to `.gitignore`
25
+
26
+ ### Changed
27
+ - `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
28
+ - `Rakefile`: `rubocop` and `rubocop_fix` tasks removed (now owned by Asgard); `flay_check` integrated into the `quality` gate
29
+
30
+ ## [0.2.6] - 2026-05-23
31
+
32
+ ### Added
33
+
34
+ - **`:compaction` hook family** — fires when `maybe_compact` determines that conversation history should be compressed. Provides `before_compaction`, `around_compaction`, `after_compaction`, and `on_compaction` callbacks.
35
+ - `on_compaction` allows an extension to supply a complete replacement message set via `ctx.compacted_messages`; when set, the core skips its own compaction strategy entirely (`ctx.handled?` returns true).
36
+ - `CompactionHookContext` — carries `robot`, `messages_before` (frozen snapshot), `config`, `strategy` (`:context_window`, `:custom`, or other symbol), `compacted_messages`, and `handled?`.
37
+ - **`:learn` hook family** — fires on every `robot.learn(text)` call with non-empty text. Provides `before_learn`, `around_learn`, `after_learn`, and `on_learn` callbacks.
38
+ - `on_learn` fires after the text has been stored and `ctx.stored = true`, giving extensions a reliable hook point for cross-session persistence.
39
+ - `LearnHookContext` — carries `robot`, `text`, `learnings_before` (frozen snapshot), and `stored`.
40
+ - **`over_compact_threshold?`** private predicate extracted from `compact_if_over_context_window` for independent testability.
41
+ - Hook tests for both new families added to `test/robot_lab/hooks_test.rb` (18 new tests across `RobotLabCompactionHooksTest` and `RobotLabLearnHooksTest`).
42
+ - Documentation for `:compaction` and `:learn` hook families added to `docs/guides/hooks.md`.
43
+
44
+ ### Changed
45
+
46
+ - **`Robot#on`** now accepts and forwards `context:` to `HookRegistry#on`, allowing extensions to pass per-registration domain config at registration time (e.g. `robot.on(MyHook, context: { domain: "finance" })`). Previously `context:` was silently dropped.
47
+ - **`maybe_compact`** refactored to dispatch through the `:compaction` hook family and delegate to `on_compaction` before falling back to the built-in strategy.
48
+ - **`Robot#learn`** refactored to dispatch through the `:learn` hook family; `on_learn` fires after deduplication with `ctx.stored` reflecting whether the text was actually added.
49
+
50
+ ### Added
51
+
52
+ - **`RobotLab::Hook` base class** (`lib/robot_lab/hook.rb`) — all hook handlers inherit from this class. Subclasses define lifecycle callbacks as `class << self` methods (`before_run`, `after_run`, etc.). Namespace is auto-derived from the class name via snake_case conversion of the final class name segment (e.g. `AuditHook` → `:audit_hook`); override with `self.namespace = :custom`. Ractor-safe by design: handler classes are Ruby constants, not Procs, so registrations are natively serializable across Ractor boundaries.
53
+ - **Hook system** — lifecycle hooks across all execution boundaries. Register handler classes with `RobotLab.on`, `network.on`, or `robot.on` for five families: `:run`, `:llm_generation`, `:tool_call`, `:network_run`, and `:task`, each with `before_*`, `around_*`, `after_*`, and (where applicable) `on_error` variants.
54
+ - `HookRegistry::Registration` — `Data.define(:handler_class, :context)`; stores a handler class reference and optional per-registration default `DotState` values. No Proc, no namespace field — namespace is read from the handler class.
55
+ - `HookContext` and five typed subclasses: `RunHookContext`, `LlmGenerationHookContext`, `ToolCallHookContext`, `NetworkRunHookContext`, `TaskHookContext` — unchanged
56
+ - `DotState` / `ExtensionState` — namespace-isolated dot-access state carried on every context object; `ctx.local` reads/writes state for the active handler's namespace
57
+ - `context:` parameter on `on(handler_class, context: {...})` — sets per-registration default `DotState` values merged in before each callback fires
58
+ - Around hooks chain correctly across handler class registrations; return value is propagated up the chain
59
+ - Per-run hooks via `robot.run("msg", hooks: MyHook)` or `robot.run("msg", hooks: [MyHook, OtherHook])`
60
+ - `on_error` fires for `:run`, `:network_run`, and `:task` families; exception is re-raised after all error handlers complete
61
+ - **`examples/xyzzy.rb`** — updated reference extension: `RobotLab::Xyzzy < RobotLab::Hook` with `class << self` methods for every hook family; logs each callback with a timestamped stdout tagline and a structured logger context snapshot; registered globally with `RobotLab.on(RobotLab::Xyzzy)`
62
+ - **Example 35** (`examples/35_hooks.rb`) — updated full hook pipeline demo: xyzzy extension tracing all hooks, `around_run` perf timer handler class, `around_llm_generation` response cache handler class (cache hits skip the LLM entirely), `before/after_llm_generation` tracer, tool call hooks, network/task hooks, and `on_error`
63
+ - **`examples/common.rb`** — added explicit `openai_api_key`, `openai_organization_id`, and `openai_project_id` to `RubyLLM.configure` to match provider configurator expectations; updated default model to `gpt-4.1-mini`
64
+
11
65
  ## [0.2.1] - 2026-05-19
12
66
 
13
67
  ### Added
data/README.md CHANGED
@@ -25,6 +25,7 @@
25
25
  - <strong>Shared Memory</strong> - Reactive key-value store with subscriptions<br>
26
26
  - <strong>Message Bus</strong> - Bidirectional robot communication via TypedBus<br>
27
27
  - <strong>Dynamic Spawning</strong> - Robots create new robots at runtime<br>
28
+ - <strong>Hook System</strong> - Lifecycle hooks across every execution boundary for instrumentation, caching, and extensions<br>
28
29
  - <strong>Layered Configuration</strong> - Cascading YAML, env vars, and RunConfig<br>
29
30
  - <strong>Rails Integration</strong> - Generators, background jobs, Turbo Stream broadcasting (via <a href="https://github.com/MadBomber/robot_lab-rails">robot_lab-rails</a>)<br>
30
31
  - <strong>Token &amp; Cost Tracking</strong> - Per-run and cumulative token counts on every robot<br>
@@ -876,6 +877,66 @@ future.robot_name # => "analyst"
876
877
  future.delegated_by # => "manager"
877
878
  ```
878
879
 
880
+ ## Hook System
881
+
882
+ RobotLab's hook system lets you intercept any point in a robot's execution pipeline without modifying framework code. Hooks use handler classes — subclasses of `RobotLab::Hook` that define lifecycle callbacks as class methods.
883
+
884
+ ```ruby
885
+ class TimerHook < RobotLab::Hook
886
+ # namespace is auto-derived: TimerHook → :timer_hook
887
+ # override with: self.namespace = :timer
888
+
889
+ def self.before_run(ctx)
890
+ ctx.local.start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
891
+ end
892
+
893
+ def self.after_run(ctx)
894
+ elapsed = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - ctx.local.start) * 1000).round(1)
895
+ puts "[timer] #{ctx.robot.name} #{elapsed}ms"
896
+ end
897
+
898
+ def self.on_error(ctx)
899
+ puts "[timer] #{ctx.robot.name} failed: #{ctx.error.message}"
900
+ end
901
+ end
902
+ ```
903
+
904
+ Register at the global, network, or robot level — or for a single call:
905
+
906
+ ```ruby
907
+ RobotLab.on(TimerHook) # every robot in this process
908
+ network.on(TimerHook) # robots inside this network only
909
+ robot.on(TimerHook) # this robot only
910
+ robot.run("msg", hooks: TimerHook) # this call only
911
+ robot.run("msg", hooks: [TimerHook, OtherHook])
912
+ ```
913
+
914
+ Pass `context:` to set default `DotState` values for the handler's namespace before each callback fires:
915
+
916
+ ```ruby
917
+ RobotLab.on(TimerHook, context: { threshold_ms: 500 })
918
+ ```
919
+
920
+ Around hooks receive a block and must call it — and return its value — so the actual work executes:
921
+
922
+ ```ruby
923
+ class PerfHook < RobotLab::Hook
924
+ self.namespace = :perf
925
+
926
+ def self.around_run(ctx, &block)
927
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
928
+ result = block.call # executes the run
929
+ ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round(1)
930
+ $stderr.puts "[perf] #{ctx.robot.name} #{ms}ms"
931
+ result # must return the result
932
+ end
933
+ end
934
+ ```
935
+
936
+ Because handler classes are Ruby constants (not Procs), all hook registrations are natively Ractor-serializable.
937
+
938
+ See the [full Hook System guide](docs/guides/hooks.md) for all hook families, context objects, and extension patterns.
939
+
879
940
  ## Extension Gems
880
941
 
881
942
  RobotLab's optional capabilities are packaged as separate gems:
data/Rakefile CHANGED
@@ -3,6 +3,8 @@
3
3
  require "bundler/gem_tasks"
4
4
  require "rake/testtask"
5
5
 
6
+ RUBOCOP_ENV = { "RUBOCOP_CACHE_ROOT" => "tmp/rubocop_cache" }.freeze
7
+
6
8
  Rake::TestTask.new(:test) do |t|
7
9
  t.libs << "test"
8
10
  t.libs << "lib"
@@ -34,16 +36,6 @@ Rake::TestTask.new(:integration) do |t|
34
36
  t.ruby_opts << "-rtest_helper"
35
37
  end
36
38
 
37
- desc "Check code style with RuboCop"
38
- task :rubocop do
39
- sh "bundle exec rubocop"
40
- end
41
-
42
- desc "Auto-correct RuboCop offenses"
43
- task :rubocop_fix do
44
- sh "bundle exec rubocop -a"
45
- end
46
-
47
39
  desc "Check code complexity with Flog (warn ≥20, fail ≥50)"
48
40
  task :flog_check do
49
41
  require 'flog'
@@ -82,7 +74,26 @@ task :flog_check do
82
74
  end
83
75
  end
84
76
 
85
- desc "Run all quality checks: tests (with coverage), RuboCop, and Flog"
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"
86
97
  task :quality do
87
98
  results = {}
88
99
 
@@ -94,13 +105,18 @@ task :quality do
94
105
  puts "\n#{'=' * 60}"
95
106
  puts "Quality Gate: RuboCop"
96
107
  puts '=' * 60
97
- results[:rubocop] = system("bundle exec rubocop") ? :pass : :fail
108
+ results[:rubocop] = system(RUBOCOP_ENV, "bundle exec rubocop") ? :pass : :fail
98
109
 
99
110
  puts "\n#{'=' * 60}"
100
111
  puts "Quality Gate: Flog Complexity"
101
112
  puts '=' * 60
102
113
  results[:flog] = system("bundle exec rake flog_check") ? :pass : :fail
103
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
+
104
120
  puts "\n#{'=' * 60}"
105
121
  puts "Quality Summary"
106
122
  puts '=' * 60
@@ -75,6 +75,7 @@ classDiagram
75
75
  | [AskUser](tool.md#built-in-askuser) | Built-in tool for terminal-based user interaction |
76
76
  | [Memory](memory.md) | Reactive key-value store for sharing data |
77
77
  | RobotMessage | Typed envelope for bus-based inter-robot communication |
78
+ | `RobotLab::Runnable` | Shared interface (`crew`, `chief`, `run`) implemented by both `Robot` and `Network` — see [Runnable Protocol](../../architecture/core-concepts.md#runnable-protocol) |
78
79
 
79
80
  ## Quick Examples
80
81
 
@@ -56,6 +56,9 @@ result = network.run(
56
56
  **context
57
57
  )
58
58
  # => SimpleFlow::Result
59
+
60
+ # Runnable protocol: a positional message works too, just like Robot#run
61
+ result = network.run("Help me", customer_id: 123)
59
62
  ```
60
63
 
61
64
  Execute the network pipeline.
@@ -64,7 +67,7 @@ Execute the network pipeline.
64
67
 
65
68
  | Name | Type | Description |
66
69
  |------|------|-------------|
67
- | `message` | `String` | The input message |
70
+ | `message` | `String`, `nil` | The input message — as the `message:` keyword, or positionally (folded into `message:` when given) |
68
71
  | `**context` | `Hash` | Additional context passed to all robots |
69
72
 
70
73
  **Returns:** `SimpleFlow::Result`
@@ -108,6 +111,15 @@ network.add_robot(robot)
108
111
 
109
112
  Add a robot without creating a pipeline task. Useful for robots referenced by other tasks.
110
113
 
114
+ ### remove_robot
115
+
116
+ ```ruby
117
+ network.remove_robot("billing")
118
+ # => Robot, or nil if no robot by that name was present
119
+ ```
120
+
121
+ Remove a dynamically-added robot from the crew by name. Complements `add_robot`. Only removes the robot from `@robots` — it does **not** rewrite the pipeline, so don't remove a robot that is a pipeline task (a `depends_on` reference to a removed robot's task name would then fail).
122
+
111
123
  ### robot / []
112
124
 
113
125
  ```ruby
@@ -294,6 +306,24 @@ elsif result.context[:technical]
294
306
  end
295
307
  ```
296
308
 
309
+ ## Runnable Protocol
310
+
311
+ `Network` includes `RobotLab::Runnable`, the shared interface it has in common with `Robot` — see [Runnable Protocol](../../architecture/network-orchestration.md#runnable-protocol) for the full picture.
312
+
313
+ | Method | Returns |
314
+ |--------|---------|
315
+ | `crew` | `robots.values` — the constituent robots, as an `Array`, in pipeline order |
316
+ | `chief` | `crew.first` — the lead robot |
317
+ | `robot_count` | `crew.size` |
318
+ | `network?` | `true` |
319
+ | `single?` | `false` |
320
+
321
+ ```ruby
322
+ network.crew.map(&:name) # => ["classifier", "billing", "technical"]
323
+ network.chief # => the first robot
324
+ network.network? # => true
325
+ ```
326
+
297
327
  ## See Also
298
328
 
299
329
  - [Creating Networks Guide](../../guides/creating-networks.md)