robot_lab 0.3.0 → 0.3.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.
Files changed (256) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/README.md +1 -1
  4. data/lib/robot_lab/mcp/transports/streamable_http.rb +1 -1
  5. data/lib/robot_lab/version.rb +1 -1
  6. metadata +3 -253
  7. data/.architecture/AGENTS.md +0 -32
  8. data/.architecture/config.yml +0 -8
  9. data/.architecture/members.yml +0 -60
  10. data/.architecture/reviews/feature-free-will.md +0 -490
  11. data/.architecture/reviews/overall-codebase.md +0 -427
  12. data/.claude/settings.local.json +0 -57
  13. data/.codex/config.toml +0 -2
  14. data/.envrc +0 -6
  15. data/.github/workflows/deploy-github-pages.yml +0 -52
  16. data/.irbrc +0 -6
  17. data/.loki +0 -65
  18. data/.quality/reek_baseline.txt +0 -43
  19. data/.rubocop.yml +0 -10
  20. data/Archspec.rb +0 -44
  21. data/CLAUDE.md +0 -141
  22. data/COMMITS.md +0 -196
  23. data/Rakefile +0 -193
  24. data/_typos.toml +0 -21
  25. data/agent2agent_review.md +0 -192
  26. data/agentf_improvements.md +0 -253
  27. data/agents.md +0 -14
  28. data/docs/api/core/index.md +0 -147
  29. data/docs/api/core/memory.md +0 -578
  30. data/docs/api/core/network.md +0 -553
  31. data/docs/api/core/result.md +0 -211
  32. data/docs/api/core/robot.md +0 -1667
  33. data/docs/api/core/state.md +0 -145
  34. data/docs/api/core/tool.md +0 -520
  35. data/docs/api/errors.md +0 -323
  36. data/docs/api/hooks.md +0 -469
  37. data/docs/api/index.md +0 -137
  38. data/docs/api/mcp/client.md +0 -387
  39. data/docs/api/mcp/index.md +0 -267
  40. data/docs/api/mcp/server.md +0 -252
  41. data/docs/api/mcp/transports.md +0 -339
  42. data/docs/api/messages/index.md +0 -126
  43. data/docs/api/messages/text-message.md +0 -170
  44. data/docs/api/messages/tool-call-message.md +0 -206
  45. data/docs/api/messages/tool-result-message.md +0 -242
  46. data/docs/api/messages/user-message.md +0 -232
  47. data/docs/api/skills.md +0 -423
  48. data/docs/api/streaming/context.md +0 -280
  49. data/docs/api/streaming/events.md +0 -185
  50. data/docs/api/streaming/index.md +0 -156
  51. data/docs/api/support.md +0 -846
  52. data/docs/architecture/core-concepts.md +0 -575
  53. data/docs/architecture/index.md +0 -252
  54. data/docs/architecture/message-flow.md +0 -366
  55. data/docs/architecture/network-orchestration.md +0 -478
  56. data/docs/architecture/robot-execution.md +0 -396
  57. data/docs/architecture/state-management.md +0 -410
  58. data/docs/assets/css/custom.css +0 -56
  59. data/docs/assets/images/robot_lab.jpg +0 -0
  60. data/docs/concepts.md +0 -641
  61. data/docs/examples/basic-chat.md +0 -258
  62. data/docs/examples/index.md +0 -425
  63. data/docs/examples/mcp-server.md +0 -403
  64. data/docs/examples/multi-robot-network.md +0 -476
  65. data/docs/examples/tool-usage.md +0 -468
  66. data/docs/getting-started/configuration.md +0 -645
  67. data/docs/getting-started/index.md +0 -56
  68. data/docs/getting-started/installation.md +0 -197
  69. data/docs/getting-started/quick-start.md +0 -293
  70. data/docs/guides/building-robots.md +0 -1187
  71. data/docs/guides/creating-networks.md +0 -736
  72. data/docs/guides/hooks.md +0 -1206
  73. data/docs/guides/index.md +0 -78
  74. data/docs/guides/knowledge.md +0 -219
  75. data/docs/guides/mcp-integration.md +0 -740
  76. data/docs/guides/memory.md +0 -458
  77. data/docs/guides/observability.md +0 -733
  78. data/docs/guides/streaming.md +0 -323
  79. data/docs/guides/using-tools.md +0 -780
  80. data/docs/index.md +0 -280
  81. data/docs/superpowers/plans/2026-05-06-agentskills.md +0 -1303
  82. data/docs/superpowers/specs/2026-05-06-agentskills-design.md +0 -247
  83. data/examples/.envrc +0 -3
  84. data/examples/01_simple_robot.rb +0 -31
  85. data/examples/02_tools.rb +0 -109
  86. data/examples/03_network.rb +0 -118
  87. data/examples/04_mcp.rb +0 -217
  88. data/examples/05_streaming.rb +0 -137
  89. data/examples/06_prompt_templates.rb +0 -325
  90. data/examples/07_network_memory.rb +0 -313
  91. data/examples/08_llm_config.rb +0 -162
  92. data/examples/09_chaining.rb +0 -251
  93. data/examples/10_memory.rb +0 -315
  94. data/examples/11_network_introspection.rb +0 -239
  95. data/examples/12_message_bus.rb +0 -67
  96. data/examples/13_spawn.rb +0 -85
  97. data/examples/14_rusty_circuit/.envrc +0 -1
  98. data/examples/14_rusty_circuit/.gitignore +0 -1
  99. data/examples/14_rusty_circuit/README.md +0 -1
  100. data/examples/14_rusty_circuit/comic.rb +0 -149
  101. data/examples/14_rusty_circuit/display.rb +0 -217
  102. data/examples/14_rusty_circuit/heckler.rb +0 -65
  103. data/examples/14_rusty_circuit/open_mic.rb +0 -132
  104. data/examples/14_rusty_circuit/prompts/open_mic_comic.md +0 -20
  105. data/examples/14_rusty_circuit/prompts/open_mic_heckler.md +0 -23
  106. data/examples/14_rusty_circuit/prompts/open_mic_scout.md +0 -20
  107. data/examples/14_rusty_circuit/scout.rb +0 -165
  108. data/examples/14_rusty_circuit/show.log +0 -234
  109. data/examples/15_memory_network_and_bus/.envrc +0 -1
  110. data/examples/15_memory_network_and_bus/README.md +0 -66
  111. data/examples/15_memory_network_and_bus/editor_in_chief.rb +0 -24
  112. data/examples/15_memory_network_and_bus/editorial_pipeline.rb +0 -213
  113. data/examples/15_memory_network_and_bus/linux_writer.rb +0 -80
  114. data/examples/15_memory_network_and_bus/os_editor.rb +0 -48
  115. data/examples/15_memory_network_and_bus/os_writer.rb +0 -49
  116. data/examples/15_memory_network_and_bus/output/combined_article.md +0 -11
  117. data/examples/15_memory_network_and_bus/output/final_article.md +0 -8
  118. data/examples/15_memory_network_and_bus/output/linux_draft.md +0 -7
  119. data/examples/15_memory_network_and_bus/output/mac_draft.md +0 -7
  120. data/examples/15_memory_network_and_bus/output/memory.json +0 -13
  121. data/examples/15_memory_network_and_bus/output/revision_1.md +0 -29
  122. data/examples/15_memory_network_and_bus/output/revision_2.md +0 -50
  123. data/examples/15_memory_network_and_bus/output/revision_3.md +0 -8
  124. data/examples/15_memory_network_and_bus/output/windows_draft.md +0 -7
  125. data/examples/15_memory_network_and_bus/prompts/os_advocate.md +0 -13
  126. data/examples/15_memory_network_and_bus/prompts/os_chief.md +0 -13
  127. data/examples/15_memory_network_and_bus/prompts/os_editor.md +0 -13
  128. data/examples/16_writers_room/.envrc +0 -1
  129. data/examples/16_writers_room/display.rb +0 -158
  130. data/examples/16_writers_room/output/.gitignore +0 -4
  131. data/examples/16_writers_room/output/README.md +0 -69
  132. data/examples/16_writers_room/output/opus_001.md +0 -263
  133. data/examples/16_writers_room/output/opus_001_notes.log +0 -470
  134. data/examples/16_writers_room/output/opus_002.md +0 -245
  135. data/examples/16_writers_room/output/opus_002_notes.log +0 -546
  136. data/examples/16_writers_room/output/opus_002_screenplay.md +0 -7989
  137. data/examples/16_writers_room/output/opus_002_screenplay_notes.md +0 -993
  138. data/examples/16_writers_room/prompts/screenplay_writer.md +0 -66
  139. data/examples/16_writers_room/prompts/writer.md +0 -37
  140. data/examples/16_writers_room/room.rb +0 -186
  141. data/examples/16_writers_room/tools.rb +0 -173
  142. data/examples/16_writers_room/writer.rb +0 -121
  143. data/examples/16_writers_room/writers_room.rb +0 -256
  144. data/examples/17_skills.rb +0 -234
  145. data/examples/18_rails/.envrc +0 -3
  146. data/examples/18_rails/.gitignore +0 -5
  147. data/examples/18_rails/Gemfile +0 -11
  148. data/examples/18_rails/README.md +0 -67
  149. data/examples/18_rails/Rakefile +0 -4
  150. data/examples/18_rails/app/controllers/application_controller.rb +0 -4
  151. data/examples/18_rails/app/controllers/chat_controller.rb +0 -50
  152. data/examples/18_rails/app/jobs/application_job.rb +0 -4
  153. data/examples/18_rails/app/jobs/robot_run_job.rb +0 -25
  154. data/examples/18_rails/app/models/application_record.rb +0 -5
  155. data/examples/18_rails/app/models/robot_lab_result.rb +0 -36
  156. data/examples/18_rails/app/models/robot_lab_thread.rb +0 -23
  157. data/examples/18_rails/app/robots/chat_robot.rb +0 -25
  158. data/examples/18_rails/app/tools/time_tool.rb +0 -9
  159. data/examples/18_rails/app/views/chat/_user_message.html.erb +0 -1
  160. data/examples/18_rails/app/views/chat/index.html.erb +0 -67
  161. data/examples/18_rails/app/views/layouts/application.html.erb +0 -49
  162. data/examples/18_rails/bin/dev +0 -7
  163. data/examples/18_rails/bin/rails +0 -6
  164. data/examples/18_rails/bin/setup +0 -15
  165. data/examples/18_rails/config/application.rb +0 -33
  166. data/examples/18_rails/config/cable.yml +0 -2
  167. data/examples/18_rails/config/database.yml +0 -5
  168. data/examples/18_rails/config/environment.rb +0 -4
  169. data/examples/18_rails/config/initializers/robot_lab.rb +0 -11
  170. data/examples/18_rails/config/routes.rb +0 -6
  171. data/examples/18_rails/config.ru +0 -4
  172. data/examples/18_rails/db/migrate/001_create_robot_lab_tables.rb +0 -32
  173. data/examples/19_token_tracking.rb +0 -138
  174. data/examples/20_circuit_breaker.rb +0 -147
  175. data/examples/21_learning_loop.rb +0 -181
  176. data/examples/22_context_compression.rb +0 -172
  177. data/examples/23_convergence.rb +0 -133
  178. data/examples/24_structured_delegation.rb +0 -151
  179. data/examples/25_history_search/conversation.jsonl +0 -30
  180. data/examples/25_history_search.rb +0 -133
  181. data/examples/26_document_store/api_versioning_adr.md +0 -52
  182. data/examples/26_document_store/incident_postmortem.md +0 -46
  183. data/examples/26_document_store/postgres_runbook.md +0 -49
  184. data/examples/26_document_store/redis_caching_guide.md +0 -48
  185. data/examples/26_document_store/sidekiq_guide.md +0 -51
  186. data/examples/26_document_store.rb +0 -140
  187. data/examples/27_incident_response/README.md +0 -65
  188. data/examples/27_incident_response/incident_response.rb +0 -261
  189. data/examples/28_mcp_discovery.rb +0 -113
  190. data/examples/29_ractor_tools.rb +0 -240
  191. data/examples/30_ractor_network.rb +0 -252
  192. data/examples/31_launch_assessment.rb +0 -246
  193. data/examples/32_newsletter_reader.rb +0 -193
  194. data/examples/33_stock_generator.rb +0 -80
  195. data/examples/33_stock_predictor.rb +0 -327
  196. data/examples/34_agentskills.rb +0 -76
  197. data/examples/35_hooks.rb +0 -266
  198. data/examples/README.md +0 -550
  199. data/examples/common.rb +0 -180
  200. data/examples/prompts/assistant.md +0 -5
  201. data/examples/prompts/audit_trail.md +0 -8
  202. data/examples/prompts/billing.md +0 -10
  203. data/examples/prompts/classifier.md +0 -7
  204. data/examples/prompts/comedian.md +0 -6
  205. data/examples/prompts/comedy_critic.md +0 -10
  206. data/examples/prompts/configurable.md +0 -9
  207. data/examples/prompts/dispatcher.md +0 -12
  208. data/examples/prompts/entity_extractor.md +0 -14
  209. data/examples/prompts/escalation.md +0 -42
  210. data/examples/prompts/frontmatter_mcp_test.md +0 -9
  211. data/examples/prompts/frontmatter_named_test.md +0 -5
  212. data/examples/prompts/frontmatter_tools_test.md +0 -6
  213. data/examples/prompts/general.md +0 -7
  214. data/examples/prompts/github_assistant.md +0 -14
  215. data/examples/prompts/helper.md +0 -4
  216. data/examples/prompts/incident_responder.md +0 -18
  217. data/examples/prompts/keyword_extractor.md +0 -11
  218. data/examples/prompts/llm_config_demo.md +0 -20
  219. data/examples/prompts/order_support.md +0 -35
  220. data/examples/prompts/os_advocate.md +0 -13
  221. data/examples/prompts/os_chief.md +0 -13
  222. data/examples/prompts/os_editor.md +0 -13
  223. data/examples/prompts/parameterized_main_test.md +0 -6
  224. data/examples/prompts/pii_redactor.md +0 -7
  225. data/examples/prompts/product_support.md +0 -37
  226. data/examples/prompts/runbook_protocol.md +0 -12
  227. data/examples/prompts/sentiment_analyzer.md +0 -12
  228. data/examples/prompts/skill_a_test.md +0 -4
  229. data/examples/prompts/skill_b_test.md +0 -4
  230. data/examples/prompts/skill_config_test.md +0 -5
  231. data/examples/prompts/skill_cycle_a_test.md +0 -6
  232. data/examples/prompts/skill_cycle_b_test.md +0 -6
  233. data/examples/prompts/skill_description_test.md +0 -4
  234. data/examples/prompts/skill_leaf_test.md +0 -4
  235. data/examples/prompts/skill_nested_test.md +0 -6
  236. data/examples/prompts/skill_refs_main_test.md +0 -6
  237. data/examples/prompts/skill_self_ref_test.md +0 -6
  238. data/examples/prompts/skill_with_mcp_test.md +0 -9
  239. data/examples/prompts/skill_with_params_test.md +0 -6
  240. data/examples/prompts/skill_with_robot_name_test.md +0 -5
  241. data/examples/prompts/skill_with_tools_test.md +0 -6
  242. data/examples/prompts/sre_compliance.md +0 -10
  243. data/examples/prompts/structured_output.md +0 -7
  244. data/examples/prompts/synthesizer.md +0 -17
  245. data/examples/prompts/technical.md +0 -10
  246. data/examples/prompts/template_with_skills_test.md +0 -6
  247. data/examples/prompts/triage.md +0 -22
  248. data/examples/ruboruby.md +0 -423
  249. data/examples/run_all.rb +0 -60
  250. data/examples/xyzzy.rb +0 -97
  251. data/logfile +0 -8
  252. data/mkdocs.yml +0 -209
  253. data/robot_concurrency.md +0 -38
  254. data/simple_acp_review.md +0 -298
  255. data/temp.md +0 -6
  256. data/tool_manifest_plan.md +0 -155
@@ -1,133 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Example 25: Chat History Search
5
- #
6
- # Demonstrates robot.search_history(query, limit:) — semantic search over a
7
- # robot's accumulated conversation turns using stemmed term-frequency cosine
8
- # similarity (classifier gem).
9
- #
10
- # The conversation fixture (30 turns across 5 topics) lives in:
11
- # examples/25_history_search/conversation.jsonl
12
- #
13
- # Usage:
14
- # ruby examples/25_history_search.rb
15
-
16
- require "json"
17
- require_relative "common"
18
-
19
- CONVERSATION_TURNS = File.readlines(
20
- File.join(__dir__, "25_history_search", "conversation.jsonl"), chomp: true
21
- ).map { |line| JSON.parse(line, symbolize_names: true) }.freeze
22
-
23
- banner "Chat History Search"
24
-
25
- # ---------------------------------------------------------------------------
26
- # Build a robot and load the conversation fixture
27
- #
28
- # Robot#replace_messages is the supported way to install a saved conversation
29
- # — checkpoint restore, replaying a transcript, or seeding a fixture like this
30
- # one. It resets the chat and re-adds each message, so there is no need to
31
- # reach into @chat and overwrite @messages by hand.
32
- # ---------------------------------------------------------------------------
33
- robot = RobotLab.build(**llm_opts, name: "tech_lead", system_prompt: "You are a senior engineering advisor.")
34
-
35
- messages = CONVERSATION_TURNS.map do |turn|
36
- RubyLLM::Message.new(role: turn[:role].to_sym, content: turn[:content])
37
- end
38
- robot.replace_messages(messages)
39
-
40
- messages = robot.messages
41
-
42
- total_words = messages.sum { |m| m.content.to_s.split.size }
43
- puts "Conversation loaded: #{messages.size} messages, ~#{total_words} words"
44
- puts "Topics: database migration, API performance, deployment pipeline, background jobs, onboarding"
45
- puts
46
-
47
- # ---------------------------------------------------------------------------
48
- # Helper: print search results
49
- # ---------------------------------------------------------------------------
50
- def show_results(results)
51
- results.each do |r|
52
- preview = r.text.length > 100 ? "#{r.text[0..97]}..." : r.text
53
- puts " [#{r.role}] score=#{format("%.3f", r.score)} idx=#{r.index}"
54
- puts " #{preview}"
55
- end
56
- end
57
-
58
- # ---------------------------------------------------------------------------
59
- # Searches across four distinct topics
60
- #
61
- # Note on TF cosine artifacts (expected behavior, not bugs):
62
- #
63
- # 'deploy rollback production incident': the rollback playbook (idx=17) ranks
64
- # 3rd rather than 1st. The short question "Should we do blue-green deploys?"
65
- # (idx=18) beats it because "deploy" appears there and TF vectors over-weight
66
- # single high-frequency query terms in short messages.
67
- #
68
- # 'caching Redis invalidation TTL': the Docker GHA cache step (idx=15) is a
69
- # false positive at rank 3 — "cache" appears in that message. The Redis hits
70
- # at ranks 1 and 2 are correct.
71
- #
72
- # These are genuine limitations of keyword-based cosine similarity. The results
73
- # shown here are authentic, not cherry-picked.
74
- # ---------------------------------------------------------------------------
75
- {
76
- "database migration schema change postgres" => 3,
77
- "slow API endpoint N+1 query performance" => 3,
78
- "deploy rollback production incident" => 3,
79
- "Sidekiq retry Stripe failed jobs dead queue" => 3,
80
- "caching Redis invalidation TTL" => 3,
81
- }.each do |query, limit|
82
- puts "── Search: '#{query}'"
83
- show_results robot.search_history(query, limit: limit)
84
- puts
85
- end
86
-
87
- # ---------------------------------------------------------------------------
88
- # RAG pattern — retrieve the most relevant turns, then inject as context
89
- # ---------------------------------------------------------------------------
90
- section "RAG Pattern: Retrieve Context, Then Call LLM"
91
- puts "(showing retrieved context — no actual LLM call)"
92
- puts
93
-
94
- rag_query = "Sidekiq jobs exhausting retries during Stripe outage"
95
- rag_hits = robot.search_history(rag_query, limit: 3)
96
- rag_ctx = rag_hits.map(&:text).join("\n\n")
97
-
98
- puts "Query: \"#{rag_query}\""
99
- puts "Retrieved #{rag_hits.size} turn(s) — #{rag_ctx.split.size} words"
100
- puts "Scores: #{rag_hits.map { |h| format("%.3f", h.score) }.join(" ")}"
101
- puts "Token savings vs. full history: ~#{total_words} words → #{rag_ctx.split.size} words"
102
- puts
103
- puts "Top retrieved turn:"
104
- puts " \"#{rag_hits.first.text[0..110]}...\""
105
- puts
106
- puts "LLM call would be:"
107
- puts ' robot.run("Prior context:\n#{context}\n\nQuestion: #{rag_query}")'
108
- puts
109
-
110
- # ---------------------------------------------------------------------------
111
- # When to use search_history
112
- # ---------------------------------------------------------------------------
113
- section "When to Use search_history"
114
- puts <<~'TEXT'
115
-
116
- Without search_history:
117
- robot.run(question)
118
- — full accumulated history sent to the LLM on every call
119
- — costs grow linearly with conversation length
120
-
121
- With search_history:
122
- hits = robot.search_history(question, limit: 3)
123
- context = hits.map(&:text).join("\n\n")
124
- robot.run("Prior context:\n#{context}\n\nQuestion: #{question}")
125
- — only the N most relevant turns are injected
126
- — token cost stays flat regardless of history length
127
- — pairs well with compress_history
128
-
129
- Optional dependency: gem "classifier", "~> 2.3"
130
-
131
- TEXT
132
-
133
- puts "Done."
@@ -1,52 +0,0 @@
1
- # Architecture Decision Record #047 — API Versioning Strategy
2
-
3
- **Status:** Accepted (2024-11-12)
4
- **Deciders:** Platform team, Mobile team, Partner integrations team
5
-
6
- ## Context
7
-
8
- The v1 API has accumulated 23 breaking changes held back by an informal freeze
9
- while three external partners built integrations. The mobile apps ship on a
10
- 4-week release cycle and cannot deploy hotfixes to force users to upgrade. We
11
- need a versioning strategy that allows the backend to evolve without coordinated
12
- lockstep releases across all consumers.
13
-
14
- ## Decision
15
-
16
- We adopt URI-based versioning (/api/v2/, /api/v3/) rather than header-based
17
- (Accept: application/vnd.company.v2+json) for the following reasons:
18
-
19
- - URI versioning is visible in logs, dashboards, and browser dev tools.
20
- - Proxy and CDN rules can target specific version prefixes.
21
- - Internal clients are all first-party and can be updated in lockstep.
22
-
23
- Header-based versioning is reserved for minor non-breaking variants (e.g.,
24
- adding optional fields) using the Prefer header.
25
-
26
- ## Support Lifecycle
27
-
28
- Each major version is supported for 18 months from GA. Deprecation notices are
29
- added to response headers (Sunset: date) 6 months before EOL. The deprecation
30
- dashboard tracks call volume per version per consumer; we do not retire a
31
- version with > 100 calls/day without direct partner outreach.
32
-
33
- ## Backwards Compatibility Rules
34
-
35
- Within a version, we **may**:
36
- - Add new fields to responses.
37
- - Add new optional request parameters.
38
- - Add new endpoints.
39
- - Add new enum values (consumers must ignore unknown values).
40
-
41
- We **must not**:
42
- - Remove or rename fields.
43
- - Change field types.
44
- - Change HTTP status codes for existing success cases.
45
- - Remove endpoints.
46
-
47
- ## Migration Tooling
48
-
49
- A version compatibility shim layer translates v1 requests to v2 internal
50
- representations and back-translates responses. This allows v1 to remain
51
- operational without duplicating business logic. The shim is tested with a
52
- contract test suite against recorded v1 response fixtures.
@@ -1,46 +0,0 @@
1
- # Incident Postmortem — INC-2024-089
2
-
3
- **Date:** 2024-10-03
4
- **Duration:** 47 minutes
5
- **Severity:** P1
6
- **Affected:** API gateway, order processing, checkout flows
7
-
8
- ## Timeline
9
-
10
- | Time | Event |
11
- |-------|-------|
12
- | 14:23 | Automated alert fires: p99 API latency exceeds 5 seconds |
13
- | 14:25 | On-call engineer pages in; confirms checkout error rate at 34% |
14
- | 14:31 | Identified spike in slow queries on orders table in Datadog APM |
15
- | 14:38 | Root cause confirmed: migration added non-concurrent index at peak traffic |
16
- | 14:44 | DBA kills the migration process; index creation aborted |
17
- | 14:48 | Query latency returns to baseline; error rate drops to 0.2% |
18
- | 15:10 | Full recovery confirmed; incident closed |
19
-
20
- ## Root Cause
21
-
22
- An engineer ran a schema migration that created an index on orders.status
23
- without the CONCURRENTLY keyword. Postgres acquired an AccessExclusiveLock on
24
- the orders table for the duration of the index build (11 minutes). All queries
25
- touching the orders table queued behind the lock, exhausting the PgBouncer
26
- connection pool within 3 minutes.
27
-
28
- ## Contributing Factors
29
-
30
- 1. Migration review checklist did not include "concurrent index" verification.
31
- 2. The migration was run manually during business hours, not via the deploy pipeline.
32
- 3. No automated linting (strong_migrations) was enforced in CI.
33
-
34
- ## Remediation (Completed)
35
-
36
- - `strong_migrations` gem added to Gemfile; CI fails on unsafe migration patterns.
37
- - Runbook updated: all migrations that touch tables > 1M rows require DBA review.
38
- - Index creation added to the concurrent-operations checklist.
39
- - PgBouncer max_client_conn increased from 150 to 300 as a buffer.
40
-
41
- ## Lessons Learned
42
-
43
- Lock acquisition during index creation is silent in application logs — the first
44
- visible symptom is connection pool exhaustion, not a database error.
45
- Instrumenting pg_locks with an alert on long-held AccessExclusiveLocks would
46
- have cut detection time from 8 minutes to under 1 minute.
@@ -1,49 +0,0 @@
1
- # PostgreSQL Operations Runbook — v3.1
2
-
3
- ## Slow Query Investigation
4
-
5
- When a query exceeds 1 second, start with pg_stat_statements:
6
-
7
- SELECT query, mean_exec_time, calls, total_exec_time
8
- FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 20;
9
-
10
- Use EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) on the top offenders.
11
- Look for Sequential Scans on large tables (> 50k rows) and Hash Joins on
12
- unindexed foreign keys. Missing index candidates appear as "rows removed by
13
- filter" values that are an order of magnitude larger than the rows returned.
14
-
15
- ## Connection Pool Exhaustion
16
-
17
- PgBouncer pools connections at the transaction level. When all connections are
18
- in use, new queries queue until pool_size is reached, at which point clients
19
- receive "too many clients" errors. Mitigate by:
20
- 1. Reducing max_connections per Rails process via database.yml pool setting.
21
- 2. Increasing server_pool_size in pgbouncer.ini incrementally.
22
- 3. Identifying and killing idle-in-transaction connections:
23
-
24
- SELECT pid, state, query, now() - query_start AS duration
25
- FROM pg_stat_activity WHERE state = 'idle in transaction'
26
- AND query_start < now() - interval '30 seconds';
27
-
28
- ## Table Bloat and Vacuum
29
-
30
- High update/delete workloads generate table bloat. Check with:
31
-
32
- SELECT relname, n_dead_tup, n_live_tup,
33
- round(n_dead_tup::numeric / nullif(n_live_tup, 0) * 100, 1) AS dead_pct
34
- FROM pg_stat_user_tables ORDER BY dead_pct DESC;
35
-
36
- If dead_pct exceeds 20% on a hot table, trigger VACUUM ANALYZE manually. For
37
- severe bloat, schedule an off-hours VACUUM FULL (acquires exclusive lock).
38
- Autovacuum scale factor defaults to 0.2; reduce to 0.05 on high-churn tables.
39
-
40
- ## Replication Lag
41
-
42
- Monitor standby lag with:
43
-
44
- SELECT client_addr, write_lag, flush_lag, replay_lag
45
- FROM pg_stat_replication;
46
-
47
- Lag above 30 seconds indicates the replica is falling behind writes. Common
48
- causes: long-running VACUUM on primary holding WAL files, network saturation
49
- between primary and replica, or index builds on the replica.
@@ -1,48 +0,0 @@
1
- # Redis Caching Patterns — Implementation Guide
2
-
3
- ## Cache Key Design
4
-
5
- Keys must encode every dimension that affects the cached value. For a
6
- user-scoped collection: `orders:user_USER_ID:page_PAGE:v2`. Always include a
7
- version suffix (v2) so a code deploy can invalidate globally by bumping the
8
- version, without a manual cache flush. Avoid encoding mutable data (e.g.,
9
- user.plan) directly in the key; use separate keys and join at read time,
10
- or accept stale reads.
11
-
12
- ## TTL Strategy
13
-
14
- Set TTLs based on acceptable staleness, not on intuition:
15
-
16
- - User session data: 24h (refreshed on activity)
17
- - API response cache (authenticated): 5 minutes
18
- - API response cache (public, CDN-backed): 60 seconds
19
- - Computed aggregates (dashboards): 15 minutes with background refresh
20
- - Feature flags: 30 seconds (fast propagation of flag changes)
21
-
22
- Always set a TTL. Unbounded keys are a production outage waiting to happen
23
- when a runaway process fills the Redis instance.
24
-
25
- ## Cache Invalidation
26
-
27
- Explicit invalidation is more reliable than TTL-only for write-heavy data. Use
28
- after_commit callbacks to delete or update cache entries when records change.
29
- For collections, track the latest updated_at timestamp as the cache key
30
- component (Russian doll caching). When multiple cache entries must be
31
- invalidated atomically, use a Redis pipeline or Lua script.
32
-
33
- ## Redis Memory Pressure
34
-
35
- When Redis hits maxmemory, it evicts keys according to the eviction policy. Use
36
- `allkeys-lru` for pure cache workloads. Monitor `evicted_keys` in Redis INFO; a
37
- non-zero and growing value means your cache is too small for the working set.
38
- Separate cache and session data into different Redis instances (or databases)
39
- so session eviction cannot be triggered by cache pressure.
40
-
41
- ## Stampede Protection
42
-
43
- Under high read concurrency, a cache miss causes multiple processes to
44
- simultaneously recompute the same expensive value — the cache stampede.
45
- Mitigate with probabilistic early expiration: recompute when TTL drops below a
46
- random fraction of the original TTL. Alternatively, use a distributed lock
47
- (Redlock or a simple SET NX PX lock key) to allow only one process to recompute
48
- while others wait briefly on the stale value.
@@ -1,51 +0,0 @@
1
- # Background Job Processing with Sidekiq — Engineering Guide
2
-
3
- ## Job Design Principles
4
-
5
- Every Sidekiq job must be idempotent: running it twice with the same arguments
6
- must produce the same outcome. This is non-negotiable because Sidekiq retries
7
- failed jobs and at-least-once delivery is guaranteed, not exactly-once. Achieve
8
- idempotency by checking preconditions (has this invoice already been generated?),
9
- using database unique constraints on job output records, and passing Stripe
10
- idempotency keys.
11
-
12
- ## Retry Configuration
13
-
14
- The default retry count is 25, which provides backoff up to ~21 days. For
15
- time-sensitive jobs (send_welcome_email) reduce to 3. For financial jobs
16
- (charge_subscription) raise to 15 to survive multi-hour outages.
17
-
18
- Configure per-job: `sidekiq_options retry: 10`
19
-
20
- Customize backoff with sidekiq_retry_in:
21
-
22
- sidekiq_retry_in { |count| (count ** 4) + 15 + rand(30) * count }
23
-
24
- This gives approximately: 15s, 1m, 5m, 17m, 34m for the first 5 retries.
25
-
26
- ## Circuit Breaker Pattern
27
-
28
- When a downstream service (Stripe, SendGrid) is degraded, jobs fail rapidly and
29
- fill the retry queue, creating a thundering-herd effect when the service
30
- recovers. Use a circuit breaker backed by Redis:
31
-
32
- - Set `stripe:circuit_open` in Redis when 3 consecutive failures occur.
33
- - In a job middleware, check the flag; if open, re-enqueue with 5-minute delay.
34
- - Auto-clear the flag after 10 minutes using Redis TTL.
35
-
36
- This converts retry churn into scheduled bursts.
37
-
38
- ## Dead Queue Management
39
-
40
- Jobs reach the dead queue after exhausting all retries. Never bulk-retry
41
- blindly. Group dead jobs by error class, inspect a sample for root cause,
42
- fix the underlying issue, then use a Rake task to re-enqueue in batches of 50
43
- with a 1-second inter-batch sleep to avoid overwhelming the recovered service.
44
- Log each re-enqueue with original args and failure reason.
45
-
46
- ## Queue Priority and Latency Budgets
47
-
48
- Define at least three queues: critical (< 1s SLA: auth, payments), default
49
- (< 30s: email, webhooks), and bulk (< 1h: exports, reports). Run dedicated
50
- Sidekiq processes per queue tier. Never mix critical and bulk work in the same
51
- process — a spike of bulk jobs will starve critical work if they share a queue.
@@ -1,140 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # Example 26: Embedding-Based Document Store
5
- #
6
- # Demonstrates Memory#store_document and Memory#search_documents — a
7
- # lightweight RAG store backed by fastembed (BAAI/bge-small-en-v1.5).
8
- #
9
- # Documents are multi-paragraph engineering guides stored as Markdown files in:
10
- # examples/26_document_store/
11
- #
12
- # Usage:
13
- # ruby examples/26_document_store.rb
14
- # (Downloads the ~23 MB ONNX model on first run; cached afterwards.)
15
-
16
- require_relative "common"
17
- require "robot_lab/document_store"
18
-
19
- banner "Embedding-Based Document Store"
20
- puts "Note: First run downloads the fastembed model (~23 MB, cached)."
21
- puts
22
-
23
- # ---------------------------------------------------------------------------
24
- # Load documents from the companion directory
25
- # ---------------------------------------------------------------------------
26
- DOC_DIR = File.join(__dir__, "26_document_store")
27
-
28
- DOCUMENTS = Dir[File.join(DOC_DIR, "*.md")].sort.each_with_object({}) do |path, h|
29
- key = File.basename(path, ".md").to_sym
30
- h[key] = File.read(path)
31
- end.freeze
32
-
33
- # ---------------------------------------------------------------------------
34
- # Store into a standalone DocumentStore
35
- # ---------------------------------------------------------------------------
36
- store = RobotLab::DocumentStore.new
37
-
38
- print "Storing #{DOCUMENTS.size} documents... "
39
- DOCUMENTS.each { |key, text| store.store(key, text) }
40
- puts "done"
41
- puts
42
- DOCUMENTS.each { |key, text| puts " #{key.to_s.ljust(24)} #{text.split.size} words" }
43
- puts
44
-
45
- # ---------------------------------------------------------------------------
46
- # Queries — each phrased differently from the document content
47
- # ---------------------------------------------------------------------------
48
- QUERIES = [
49
- {
50
- label: "Database query performance",
51
- query: "Why is my Postgres query slow and how do I investigate it?",
52
- want: :postgres_runbook
53
- },
54
- {
55
- label: "Background job failures during outage",
56
- query: "Jobs keep failing when Stripe is down. How do I stop them piling up?",
57
- want: :sidekiq_guide
58
- },
59
- {
60
- label: "API breaking changes policy",
61
- query: "Can I rename a response field in the API without breaking clients?",
62
- want: :api_versioning_adr
63
- },
64
- {
65
- label: "Cache expiry and memory pressure",
66
- query: "Redis is evicting keys unexpectedly and the cache hit rate has dropped.",
67
- want: :redis_caching_guide
68
- },
69
- {
70
- label: "Production outage from table lock",
71
- query: "We had an outage caused by a database lock during a migration. What happened?",
72
- want: :incident_postmortem
73
- },
74
- {
75
- label: "Semantic gap — no shared keywords",
76
- query: "Connection pool is full and new requests are being rejected.",
77
- want: :postgres_runbook
78
- },
79
- ].freeze
80
-
81
- QUERIES.each do |q|
82
- results = store.search(q[:query], limit: 3)
83
- top = results.first
84
- verdict = top[:key] == q[:want] ? "✓ correct" : "✗ expected #{q[:want]}"
85
-
86
- puts "── #{q[:label]}"
87
- puts " Query: \"#{q[:query]}\""
88
- puts " Top result: #{top[:key]} (#{format("%.3f", top[:score])}) — #{verdict}"
89
- puts " Ranking: " + results.map { |r| "#{r[:key]} #{format("%.3f", r[:score])}" }.join(" | ")
90
- puts
91
- end
92
-
93
- # ---------------------------------------------------------------------------
94
- # Delete and verify
95
- # ---------------------------------------------------------------------------
96
- section "Delete :redis_caching_guide, Re-run Cache Query"
97
- store.delete(:redis_caching_guide)
98
- results = store.search("Redis evicting keys unexpectedly", limit: 2)
99
- puts " Remaining keys: #{store.keys.inspect}"
100
- puts " Top result after deletion: #{results.first[:key]}"
101
- puts
102
-
103
- # ---------------------------------------------------------------------------
104
- # Memory integration
105
- # ---------------------------------------------------------------------------
106
- section "Memory Integration"
107
- memory = RobotLab::Memory.new(enable_cache: false)
108
-
109
- DOCUMENTS.each { |key, text| memory.store_document(key, text) }
110
- puts " Stored #{memory.document_keys.size} documents via memory.store_document"
111
-
112
- hits = memory.search_documents("slow query bloat vacuum autovacuum", limit: 2)
113
- puts " Search 'slow query bloat vacuum autovacuum':"
114
- hits.each { |h| puts " #{h[:key]} (#{format("%.3f", h[:score])})" }
115
-
116
- memory.delete_document(:postgres_runbook)
117
- puts " After delete, keys: #{memory.document_keys.inspect}"
118
- puts
119
-
120
- # ---------------------------------------------------------------------------
121
- # RAG pattern
122
- # ---------------------------------------------------------------------------
123
- section "RAG Pattern: Retrieve Relevant Docs, Then Generate with LLM"
124
-
125
- rag_query = "Our Sidekiq jobs exhaust retries and land in the dead queue after a Stripe outage."
126
-
127
- hits = store.search(rag_query, limit: 2)
128
- context = hits.map { |h| h[:text] }.join("\n\n---\n\n")
129
-
130
- puts "User question:"
131
- puts " \"#{rag_query}\""
132
- puts
133
- puts "Retrieved #{hits.size} document(s) — #{context.split.size} words of context:"
134
- hits.each { |h| puts " #{h[:key]} (score #{format("%.3f", h[:score])})" }
135
- puts
136
- puts "LLM call would be:"
137
- puts ' robot.run("Use the following docs:\n#{context}\n\nQuestion: #{rag_query}")'
138
- puts
139
-
140
- puts "Done."
@@ -1,65 +0,0 @@
1
- # Example 27: Production Incident War Room
2
-
3
- A simulated payment-service outage, used to demonstrate RobotLab's Phase 5
4
- infrastructure features: **reactive memory**, **BusPoller serialized
5
- delivery**, and **poller groups**.
6
-
7
- ## What it does
8
-
9
- A network runs four robots against one incident ("elevated HTTP 500s, p99
10
- latency spiked to 8s"):
11
-
12
- - **Three SRE scouts** (`db_scout`, `net_scout`, `app_scout`) investigate the
13
- database, network, and application layers in parallel. Each makes one LLM
14
- call, writes its two-sentence finding to shared **reactive memory**
15
- (`:db_finding`, `:net_finding`, `:app_finding`), and broadcasts a status
16
- line to the war room over TypedBus.
17
- - **The war room** receives those bus messages through **BusPoller**, which
18
- serializes delivery — if two scouts finish at the same instant, their
19
- updates queue and are processed one at a time, in arrival order, with no
20
- re-entrancy and nothing dropped.
21
- - **The incident commander** depends on all three scouts and blocks on
22
- `memory.get(:db_finding, :net_finding, :app_finding, wait: 60)` — an
23
- IO.pipe-backed waiter woken by `IO.select`, so there is no busy-wait and it
24
- cooperates with Async. When all findings land it makes one synthesis call
25
- and writes a 3-5 bullet action plan.
26
-
27
- Also on display:
28
-
29
- - **Poller groups** — the scouts run in `poller_group: :investigation`, the
30
- commander in `:command`, declared per task on the network.
31
- - **Memory subscriptions** — a `memory.subscribe` callback prints a line as
32
- each finding is written.
33
- - **Timeout degradation** — `Memory#get(wait:)` raises
34
- `RobotLab::AwaitTimeout` rather than returning a sentinel; the commander
35
- rescues it and re-reads without `wait:`, so a slow scout degrades the
36
- report instead of killing the run.
37
-
38
- ## How to run
39
-
40
- From the gem root (an LM Studio server must be running — `lms server start`;
41
- provider/model come from `examples/common.rb`):
42
-
43
- ```bash
44
- bundle exec ruby examples/27_incident_response/incident_response.rb
45
- ```
46
-
47
- Note that `examples/run_all.rb` does **not** include this demo — it only runs
48
- the top-level `NN_*.rb` files, so run it explicitly as above.
49
-
50
- The run makes four LLM calls (three scout diagnoses plus the commander's
51
- synthesis), so expect a few minutes on the default local model;
52
- `LLM_PROFILE=small` shortens it.
53
-
54
- ## Output
55
-
56
- - Console: the network graph, each memory write as it happens, the war-room
57
- updates in BusPoller delivery order, the scouts' findings, and the final
58
- action plan.
59
- - `output/incident_report.md` — the commander's incident action plan.
60
-
61
- ## Files
62
-
63
- - `incident_response.rb` — the whole demo: `SREScout`, `WarRoom`, and
64
- `IncidentCommander` robot classes plus network wiring and the run itself
65
- - `output/` — where the incident report is written