lex-agentic-self 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.
Files changed (249) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +12 -0
  3. data/Gemfile +5 -0
  4. data/LICENSE +21 -0
  5. data/README.md +13 -0
  6. data/lex-agentic-self.gemspec +31 -0
  7. data/lib/legion/extensions/agentic/self/agency/client.rb +21 -0
  8. data/lib/legion/extensions/agentic/self/agency/helpers/constants.rb +77 -0
  9. data/lib/legion/extensions/agentic/self/agency/helpers/efficacy_model.rb +136 -0
  10. data/lib/legion/extensions/agentic/self/agency/helpers/outcome_event.rb +52 -0
  11. data/lib/legion/extensions/agentic/self/agency/runners/agency.rb +117 -0
  12. data/lib/legion/extensions/agentic/self/agency/version.rb +13 -0
  13. data/lib/legion/extensions/agentic/self/agency.rb +19 -0
  14. data/lib/legion/extensions/agentic/self/anchor/client.rb +15 -0
  15. data/lib/legion/extensions/agentic/self/anchor/helpers/anchor.rb +92 -0
  16. data/lib/legion/extensions/agentic/self/anchor/helpers/anchor_engine.rb +123 -0
  17. data/lib/legion/extensions/agentic/self/anchor/helpers/chain.rb +93 -0
  18. data/lib/legion/extensions/agentic/self/anchor/helpers/constants.rb +46 -0
  19. data/lib/legion/extensions/agentic/self/anchor/runners/cognitive_anchor.rb +70 -0
  20. data/lib/legion/extensions/agentic/self/anchor/version.rb +13 -0
  21. data/lib/legion/extensions/agentic/self/anchor.rb +22 -0
  22. data/lib/legion/extensions/agentic/self/anosognosia/client.rb +28 -0
  23. data/lib/legion/extensions/agentic/self/anosognosia/helpers/anosognosia_engine.rb +153 -0
  24. data/lib/legion/extensions/agentic/self/anosognosia/helpers/cognitive_deficit.rb +71 -0
  25. data/lib/legion/extensions/agentic/self/anosognosia/helpers/constants.rb +29 -0
  26. data/lib/legion/extensions/agentic/self/anosognosia/runners/anosognosia.rb +98 -0
  27. data/lib/legion/extensions/agentic/self/anosognosia/version.rb +13 -0
  28. data/lib/legion/extensions/agentic/self/anosognosia.rb +19 -0
  29. data/lib/legion/extensions/agentic/self/architecture/client.rb +19 -0
  30. data/lib/legion/extensions/agentic/self/architecture/helpers/architecture_engine.rb +167 -0
  31. data/lib/legion/extensions/agentic/self/architecture/helpers/connection.rb +57 -0
  32. data/lib/legion/extensions/agentic/self/architecture/helpers/constants.rb +37 -0
  33. data/lib/legion/extensions/agentic/self/architecture/helpers/subsystem.rb +80 -0
  34. data/lib/legion/extensions/agentic/self/architecture/runners/cognitive_architecture.rb +125 -0
  35. data/lib/legion/extensions/agentic/self/architecture/version.rb +13 -0
  36. data/lib/legion/extensions/agentic/self/architecture.rb +20 -0
  37. data/lib/legion/extensions/agentic/self/default_mode_network/actors/idle.rb +45 -0
  38. data/lib/legion/extensions/agentic/self/default_mode_network/client.rb +28 -0
  39. data/lib/legion/extensions/agentic/self/default_mode_network/helpers/constants.rb +53 -0
  40. data/lib/legion/extensions/agentic/self/default_mode_network/helpers/dmn_engine.rb +221 -0
  41. data/lib/legion/extensions/agentic/self/default_mode_network/helpers/wandering_thought.rb +60 -0
  42. data/lib/legion/extensions/agentic/self/default_mode_network/runners/default_mode_network.rb +122 -0
  43. data/lib/legion/extensions/agentic/self/default_mode_network/version.rb +13 -0
  44. data/lib/legion/extensions/agentic/self/default_mode_network.rb +20 -0
  45. data/lib/legion/extensions/agentic/self/fingerprint/client.rb +28 -0
  46. data/lib/legion/extensions/agentic/self/fingerprint/helpers/cognitive_trait.rb +73 -0
  47. data/lib/legion/extensions/agentic/self/fingerprint/helpers/constants.rb +60 -0
  48. data/lib/legion/extensions/agentic/self/fingerprint/helpers/fingerprint_engine.rb +169 -0
  49. data/lib/legion/extensions/agentic/self/fingerprint/runners/cognitive_fingerprint.rb +86 -0
  50. data/lib/legion/extensions/agentic/self/fingerprint/version.rb +13 -0
  51. data/lib/legion/extensions/agentic/self/fingerprint.rb +19 -0
  52. data/lib/legion/extensions/agentic/self/identity/actors/credential_refresh.rb +49 -0
  53. data/lib/legion/extensions/agentic/self/identity/actors/orphan_check.rb +52 -0
  54. data/lib/legion/extensions/agentic/self/identity/client.rb +27 -0
  55. data/lib/legion/extensions/agentic/self/identity/helpers/dimensions.rb +75 -0
  56. data/lib/legion/extensions/agentic/self/identity/helpers/fingerprint.rb +170 -0
  57. data/lib/legion/extensions/agentic/self/identity/helpers/graph_client.rb +29 -0
  58. data/lib/legion/extensions/agentic/self/identity/helpers/graph_token.rb +36 -0
  59. data/lib/legion/extensions/agentic/self/identity/helpers/token_cache.rb +59 -0
  60. data/lib/legion/extensions/agentic/self/identity/helpers/vault_secrets.rb +80 -0
  61. data/lib/legion/extensions/agentic/self/identity/local_migrations/20260316000030_create_fingerprint.rb +20 -0
  62. data/lib/legion/extensions/agentic/self/identity/runners/entra.rb +402 -0
  63. data/lib/legion/extensions/agentic/self/identity/runners/identity.rb +90 -0
  64. data/lib/legion/extensions/agentic/self/identity/version.rb +13 -0
  65. data/lib/legion/extensions/agentic/self/identity.rb +28 -0
  66. data/lib/legion/extensions/agentic/self/metacognition/client.rb +27 -0
  67. data/lib/legion/extensions/agentic/self/metacognition/helpers/constants.rb +377 -0
  68. data/lib/legion/extensions/agentic/self/metacognition/helpers/narrator_bridge.rb +85 -0
  69. data/lib/legion/extensions/agentic/self/metacognition/helpers/registry_store.rb +70 -0
  70. data/lib/legion/extensions/agentic/self/metacognition/helpers/self_model.rb +160 -0
  71. data/lib/legion/extensions/agentic/self/metacognition/helpers/snapshot_store.rb +82 -0
  72. data/lib/legion/extensions/agentic/self/metacognition/runners/metacognition.rb +116 -0
  73. data/lib/legion/extensions/agentic/self/metacognition/runners/registry.rb +180 -0
  74. data/lib/legion/extensions/agentic/self/metacognition/version.rb +13 -0
  75. data/lib/legion/extensions/agentic/self/metacognition.rb +22 -0
  76. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/client.rb +25 -0
  77. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/helpers/calibration_tracker.rb +96 -0
  78. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/helpers/constants.rb +47 -0
  79. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/helpers/monitoring_engine.rb +141 -0
  80. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/helpers/monitoring_judgment.rb +79 -0
  81. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/runners/metacognitive_monitoring.rb +151 -0
  82. data/lib/legion/extensions/agentic/self/metacognitive_monitoring/version.rb +13 -0
  83. data/lib/legion/extensions/agentic/self/metacognitive_monitoring.rb +20 -0
  84. data/lib/legion/extensions/agentic/self/narrative_arc/client.rb +29 -0
  85. data/lib/legion/extensions/agentic/self/narrative_arc/helpers/arc.rb +137 -0
  86. data/lib/legion/extensions/agentic/self/narrative_arc/helpers/arc_engine.rb +119 -0
  87. data/lib/legion/extensions/agentic/self/narrative_arc/helpers/beat_event.rb +59 -0
  88. data/lib/legion/extensions/agentic/self/narrative_arc/helpers/constants.rb +66 -0
  89. data/lib/legion/extensions/agentic/self/narrative_arc/runners/narrative.rb +101 -0
  90. data/lib/legion/extensions/agentic/self/narrative_arc/version.rb +13 -0
  91. data/lib/legion/extensions/agentic/self/narrative_arc.rb +20 -0
  92. data/lib/legion/extensions/agentic/self/narrative_identity/actors/narrative_decay.rb +45 -0
  93. data/lib/legion/extensions/agentic/self/narrative_identity/client.rb +22 -0
  94. data/lib/legion/extensions/agentic/self/narrative_identity/helpers/chapter.rb +48 -0
  95. data/lib/legion/extensions/agentic/self/narrative_identity/helpers/constants.rb +62 -0
  96. data/lib/legion/extensions/agentic/self/narrative_identity/helpers/episode.rb +67 -0
  97. data/lib/legion/extensions/agentic/self/narrative_identity/helpers/narrative_engine.rb +187 -0
  98. data/lib/legion/extensions/agentic/self/narrative_identity/helpers/theme.rb +50 -0
  99. data/lib/legion/extensions/agentic/self/narrative_identity/runners/narrative_identity.rb +158 -0
  100. data/lib/legion/extensions/agentic/self/narrative_identity/version.rb +13 -0
  101. data/lib/legion/extensions/agentic/self/narrative_identity.rb +21 -0
  102. data/lib/legion/extensions/agentic/self/narrative_self/client.rb +27 -0
  103. data/lib/legion/extensions/agentic/self/narrative_self/helpers/autobiography.rb +187 -0
  104. data/lib/legion/extensions/agentic/self/narrative_self/helpers/constants.rb +42 -0
  105. data/lib/legion/extensions/agentic/self/narrative_self/helpers/episode.rb +81 -0
  106. data/lib/legion/extensions/agentic/self/narrative_self/helpers/narrative_thread.rb +65 -0
  107. data/lib/legion/extensions/agentic/self/narrative_self/runners/narrative_self.rb +86 -0
  108. data/lib/legion/extensions/agentic/self/narrative_self/version.rb +13 -0
  109. data/lib/legion/extensions/agentic/self/narrative_self.rb +20 -0
  110. data/lib/legion/extensions/agentic/self/personality/client.rb +21 -0
  111. data/lib/legion/extensions/agentic/self/personality/helpers/constants.rb +84 -0
  112. data/lib/legion/extensions/agentic/self/personality/helpers/personality_store.rb +126 -0
  113. data/lib/legion/extensions/agentic/self/personality/helpers/trait_model.rb +147 -0
  114. data/lib/legion/extensions/agentic/self/personality/runners/personality.rb +102 -0
  115. data/lib/legion/extensions/agentic/self/personality/version.rb +13 -0
  116. data/lib/legion/extensions/agentic/self/personality.rb +19 -0
  117. data/lib/legion/extensions/agentic/self/reflection/client.rb +27 -0
  118. data/lib/legion/extensions/agentic/self/reflection/helpers/constants.rb +66 -0
  119. data/lib/legion/extensions/agentic/self/reflection/helpers/llm_enhancer.rb +166 -0
  120. data/lib/legion/extensions/agentic/self/reflection/helpers/monitors.rb +186 -0
  121. data/lib/legion/extensions/agentic/self/reflection/helpers/reflection.rb +54 -0
  122. data/lib/legion/extensions/agentic/self/reflection/helpers/reflection_store.rb +99 -0
  123. data/lib/legion/extensions/agentic/self/reflection/runners/reflection.rb +199 -0
  124. data/lib/legion/extensions/agentic/self/reflection/version.rb +13 -0
  125. data/lib/legion/extensions/agentic/self/reflection.rb +21 -0
  126. data/lib/legion/extensions/agentic/self/self_model/client.rb +19 -0
  127. data/lib/legion/extensions/agentic/self/self_model/helpers/capability.rb +93 -0
  128. data/lib/legion/extensions/agentic/self/self_model/helpers/constants.rb +46 -0
  129. data/lib/legion/extensions/agentic/self/self_model/helpers/knowledge_domain.rb +82 -0
  130. data/lib/legion/extensions/agentic/self/self_model/helpers/self_model.rb +150 -0
  131. data/lib/legion/extensions/agentic/self/self_model/runners/self_model.rb +82 -0
  132. data/lib/legion/extensions/agentic/self/self_model/version.rb +13 -0
  133. data/lib/legion/extensions/agentic/self/self_model.rb +21 -0
  134. data/lib/legion/extensions/agentic/self/self_talk/actors/volume_decay.rb +45 -0
  135. data/lib/legion/extensions/agentic/self/self_talk/client.rb +30 -0
  136. data/lib/legion/extensions/agentic/self/self_talk/helpers/constants.rb +63 -0
  137. data/lib/legion/extensions/agentic/self/self_talk/helpers/dialogue.rb +114 -0
  138. data/lib/legion/extensions/agentic/self/self_talk/helpers/dialogue_turn.rb +43 -0
  139. data/lib/legion/extensions/agentic/self/self_talk/helpers/inner_voice.rb +77 -0
  140. data/lib/legion/extensions/agentic/self/self_talk/helpers/llm_enhancer.rb +135 -0
  141. data/lib/legion/extensions/agentic/self/self_talk/helpers/self_talk_engine.rb +160 -0
  142. data/lib/legion/extensions/agentic/self/self_talk/runners/self_talk.rb +172 -0
  143. data/lib/legion/extensions/agentic/self/self_talk/version.rb +13 -0
  144. data/lib/legion/extensions/agentic/self/self_talk.rb +22 -0
  145. data/lib/legion/extensions/agentic/self/version.rb +11 -0
  146. data/lib/legion/extensions/agentic/self.rb +33 -0
  147. data/spec/legion/extensions/agentic/self/agency/client_spec.rb +67 -0
  148. data/spec/legion/extensions/agentic/self/agency/helpers/constants_spec.rb +73 -0
  149. data/spec/legion/extensions/agentic/self/agency/helpers/efficacy_model_spec.rb +190 -0
  150. data/spec/legion/extensions/agentic/self/agency/helpers/outcome_event_spec.rb +85 -0
  151. data/spec/legion/extensions/agentic/self/agency/runners/agency_spec.rb +132 -0
  152. data/spec/legion/extensions/agentic/self/anchor/client_spec.rb +30 -0
  153. data/spec/legion/extensions/agentic/self/anchor/helpers/anchor_engine_spec.rb +109 -0
  154. data/spec/legion/extensions/agentic/self/anchor/helpers/anchor_spec.rb +124 -0
  155. data/spec/legion/extensions/agentic/self/anchor/helpers/chain_spec.rb +106 -0
  156. data/spec/legion/extensions/agentic/self/anchor/helpers/constants_spec.rb +53 -0
  157. data/spec/legion/extensions/agentic/self/anchor/runners/cognitive_anchor_spec.rb +70 -0
  158. data/spec/legion/extensions/agentic/self/anosognosia/anosognosia_spec.rb +15 -0
  159. data/spec/legion/extensions/agentic/self/anosognosia/client_spec.rb +50 -0
  160. data/spec/legion/extensions/agentic/self/anosognosia/helpers/anosognosia_engine_spec.rb +266 -0
  161. data/spec/legion/extensions/agentic/self/anosognosia/helpers/cognitive_deficit_spec.rb +150 -0
  162. data/spec/legion/extensions/agentic/self/anosognosia/helpers/constants_spec.rb +58 -0
  163. data/spec/legion/extensions/agentic/self/anosognosia/runners/anosognosia_spec.rb +225 -0
  164. data/spec/legion/extensions/agentic/self/architecture/client_spec.rb +51 -0
  165. data/spec/legion/extensions/agentic/self/architecture/helpers/architecture_engine_spec.rb +321 -0
  166. data/spec/legion/extensions/agentic/self/architecture/helpers/connection_spec.rb +118 -0
  167. data/spec/legion/extensions/agentic/self/architecture/helpers/subsystem_spec.rb +189 -0
  168. data/spec/legion/extensions/agentic/self/architecture/runners/cognitive_architecture_spec.rb +181 -0
  169. data/spec/legion/extensions/agentic/self/default_mode_network/client_spec.rb +69 -0
  170. data/spec/legion/extensions/agentic/self/default_mode_network/helpers/constants_spec.rb +76 -0
  171. data/spec/legion/extensions/agentic/self/default_mode_network/helpers/dmn_engine_spec.rb +321 -0
  172. data/spec/legion/extensions/agentic/self/default_mode_network/helpers/wandering_thought_spec.rb +145 -0
  173. data/spec/legion/extensions/agentic/self/default_mode_network/runners/default_mode_network_spec.rb +269 -0
  174. data/spec/legion/extensions/agentic/self/fingerprint/client_spec.rb +54 -0
  175. data/spec/legion/extensions/agentic/self/fingerprint/helpers/cognitive_trait_spec.rb +180 -0
  176. data/spec/legion/extensions/agentic/self/fingerprint/helpers/constants_spec.rb +108 -0
  177. data/spec/legion/extensions/agentic/self/fingerprint/helpers/fingerprint_engine_spec.rb +318 -0
  178. data/spec/legion/extensions/agentic/self/fingerprint/runners/cognitive_fingerprint_spec.rb +232 -0
  179. data/spec/legion/extensions/agentic/self/identity/actors/orphan_check_spec.rb +104 -0
  180. data/spec/legion/extensions/agentic/self/identity/client_spec.rb +32 -0
  181. data/spec/legion/extensions/agentic/self/identity/helpers/dimensions_spec.rb +51 -0
  182. data/spec/legion/extensions/agentic/self/identity/helpers/fingerprint_spec.rb +66 -0
  183. data/spec/legion/extensions/agentic/self/identity/helpers/graph_client_spec.rb +19 -0
  184. data/spec/legion/extensions/agentic/self/identity/helpers/graph_token_spec.rb +31 -0
  185. data/spec/legion/extensions/agentic/self/identity/helpers/token_cache_spec.rb +50 -0
  186. data/spec/legion/extensions/agentic/self/identity/local_persistence_spec.rb +329 -0
  187. data/spec/legion/extensions/agentic/self/identity/runners/entra_spec.rb +655 -0
  188. data/spec/legion/extensions/agentic/self/identity/runners/identity_spec.rb +61 -0
  189. data/spec/legion/extensions/agentic/self/metacognition/client_spec.rb +20 -0
  190. data/spec/legion/extensions/agentic/self/metacognition/helpers/constants_spec.rb +31 -0
  191. data/spec/legion/extensions/agentic/self/metacognition/helpers/narrator_bridge_spec.rb +102 -0
  192. data/spec/legion/extensions/agentic/self/metacognition/helpers/registry_store_spec.rb +227 -0
  193. data/spec/legion/extensions/agentic/self/metacognition/helpers/self_model_spec.rb +117 -0
  194. data/spec/legion/extensions/agentic/self/metacognition/helpers/snapshot_store_spec.rb +128 -0
  195. data/spec/legion/extensions/agentic/self/metacognition/runners/metacognition_spec.rb +110 -0
  196. data/spec/legion/extensions/agentic/self/metacognition/runners/registry_spec.rb +281 -0
  197. data/spec/legion/extensions/agentic/self/metacognitive_monitoring/client_spec.rb +59 -0
  198. data/spec/legion/extensions/agentic/self/metacognitive_monitoring/helpers/calibration_tracker_spec.rb +143 -0
  199. data/spec/legion/extensions/agentic/self/metacognitive_monitoring/helpers/constants_spec.rb +91 -0
  200. data/spec/legion/extensions/agentic/self/metacognitive_monitoring/helpers/monitoring_engine_spec.rb +198 -0
  201. data/spec/legion/extensions/agentic/self/metacognitive_monitoring/helpers/monitoring_judgment_spec.rb +172 -0
  202. data/spec/legion/extensions/agentic/self/metacognitive_monitoring/runners/metacognitive_monitoring_spec.rb +244 -0
  203. data/spec/legion/extensions/agentic/self/narrative_arc/client_spec.rb +22 -0
  204. data/spec/legion/extensions/agentic/self/narrative_arc/helpers/arc_engine_spec.rb +183 -0
  205. data/spec/legion/extensions/agentic/self/narrative_arc/helpers/arc_spec.rb +177 -0
  206. data/spec/legion/extensions/agentic/self/narrative_arc/helpers/beat_event_spec.rb +96 -0
  207. data/spec/legion/extensions/agentic/self/narrative_arc/helpers/constants_spec.rb +75 -0
  208. data/spec/legion/extensions/agentic/self/narrative_arc/runners/narrative_spec.rb +142 -0
  209. data/spec/legion/extensions/agentic/self/narrative_identity/client_spec.rb +69 -0
  210. data/spec/legion/extensions/agentic/self/narrative_identity/helpers/chapter_spec.rb +85 -0
  211. data/spec/legion/extensions/agentic/self/narrative_identity/helpers/constants_spec.rb +83 -0
  212. data/spec/legion/extensions/agentic/self/narrative_identity/helpers/episode_spec.rb +180 -0
  213. data/spec/legion/extensions/agentic/self/narrative_identity/helpers/narrative_engine_spec.rb +307 -0
  214. data/spec/legion/extensions/agentic/self/narrative_identity/helpers/theme_spec.rb +107 -0
  215. data/spec/legion/extensions/agentic/self/narrative_identity/runners/narrative_identity_spec.rb +240 -0
  216. data/spec/legion/extensions/agentic/self/narrative_self/client_spec.rb +67 -0
  217. data/spec/legion/extensions/agentic/self/narrative_self/helpers/autobiography_spec.rb +155 -0
  218. data/spec/legion/extensions/agentic/self/narrative_self/helpers/constants_spec.rb +28 -0
  219. data/spec/legion/extensions/agentic/self/narrative_self/helpers/episode_spec.rb +144 -0
  220. data/spec/legion/extensions/agentic/self/narrative_self/helpers/narrative_thread_spec.rb +87 -0
  221. data/spec/legion/extensions/agentic/self/narrative_self/runners/narrative_self_spec.rb +118 -0
  222. data/spec/legion/extensions/agentic/self/personality/client_spec.rb +20 -0
  223. data/spec/legion/extensions/agentic/self/personality/helpers/constants_spec.rb +41 -0
  224. data/spec/legion/extensions/agentic/self/personality/helpers/personality_store_spec.rb +66 -0
  225. data/spec/legion/extensions/agentic/self/personality/helpers/trait_model_spec.rb +148 -0
  226. data/spec/legion/extensions/agentic/self/personality/runners/personality_spec.rb +67 -0
  227. data/spec/legion/extensions/agentic/self/reflection/client_spec.rb +24 -0
  228. data/spec/legion/extensions/agentic/self/reflection/helpers/llm_enhancer_spec.rb +191 -0
  229. data/spec/legion/extensions/agentic/self/reflection/helpers/monitors_spec.rb +120 -0
  230. data/spec/legion/extensions/agentic/self/reflection/helpers/reflection_spec.rb +49 -0
  231. data/spec/legion/extensions/agentic/self/reflection/helpers/reflection_store_spec.rb +93 -0
  232. data/spec/legion/extensions/agentic/self/reflection/runners/reflection_spec.rb +204 -0
  233. data/spec/legion/extensions/agentic/self/self_model/client_spec.rb +55 -0
  234. data/spec/legion/extensions/agentic/self/self_model/helpers/capability_spec.rb +160 -0
  235. data/spec/legion/extensions/agentic/self/self_model/helpers/knowledge_domain_spec.rb +128 -0
  236. data/spec/legion/extensions/agentic/self/self_model/helpers/self_model_spec.rb +238 -0
  237. data/spec/legion/extensions/agentic/self/self_model/runners/self_model_spec.rb +143 -0
  238. data/spec/legion/extensions/agentic/self/self_talk/actors/volume_decay_spec.rb +46 -0
  239. data/spec/legion/extensions/agentic/self/self_talk/client_spec.rb +26 -0
  240. data/spec/legion/extensions/agentic/self/self_talk/helpers/constants_spec.rb +110 -0
  241. data/spec/legion/extensions/agentic/self/self_talk/helpers/dialogue_spec.rb +191 -0
  242. data/spec/legion/extensions/agentic/self/self_talk/helpers/dialogue_turn_spec.rb +78 -0
  243. data/spec/legion/extensions/agentic/self/self_talk/helpers/inner_voice_spec.rb +172 -0
  244. data/spec/legion/extensions/agentic/self/self_talk/helpers/llm_enhancer_spec.rb +206 -0
  245. data/spec/legion/extensions/agentic/self/self_talk/helpers/self_talk_engine_spec.rb +239 -0
  246. data/spec/legion/extensions/agentic/self/self_talk/runners/self_talk_llm_spec.rb +169 -0
  247. data/spec/legion/extensions/agentic/self/self_talk/runners/self_talk_spec.rb +196 -0
  248. data/spec/spec_helper.rb +46 -0
  249. metadata +347 -0
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Self::SelfTalk::Helpers::Dialogue do
4
+ subject(:dialogue) { described_class.new(topic: 'Should I refactor the cache layer?') }
5
+
6
+ let(:voice_id) { 'voice-abc' }
7
+
8
+ describe '#initialize' do
9
+ it 'assigns a UUID id' do
10
+ expect(dialogue.id).to match(/\A[0-9a-f-]{36}\z/)
11
+ end
12
+
13
+ it 'assigns topic' do
14
+ expect(dialogue.topic).to eq('Should I refactor the cache layer?')
15
+ end
16
+
17
+ it 'starts with empty turns' do
18
+ expect(dialogue.turns).to be_empty
19
+ end
20
+
21
+ it 'starts with :open status' do
22
+ expect(dialogue.status).to eq(:open)
23
+ end
24
+
25
+ it 'starts with nil conclusion' do
26
+ expect(dialogue.conclusion).to be_nil
27
+ end
28
+
29
+ it 'sets created_at' do
30
+ expect(dialogue.created_at).to be_a(Time)
31
+ end
32
+ end
33
+
34
+ describe '#add_turn!' do
35
+ it 'adds a turn and returns the DialogueTurn' do
36
+ turn = dialogue.add_turn!(voice_id: voice_id, content: 'Yes, it needs refactoring')
37
+ expect(turn).to be_a(Legion::Extensions::Agentic::Self::SelfTalk::Helpers::DialogueTurn)
38
+ expect(dialogue.turn_count).to eq(1)
39
+ end
40
+
41
+ it 'returns false when dialogue is not active' do
42
+ dialogue.conclude!('done')
43
+ result = dialogue.add_turn!(voice_id: voice_id, content: 'Late thought')
44
+ expect(result).to be false
45
+ end
46
+
47
+ it 'returns false when turn limit is reached' do
48
+ stub_const('Legion::Extensions::Agentic::Self::SelfTalk::Helpers::Constants::MAX_TURNS_PER_DIALOGUE', 2)
49
+ dialogue.add_turn!(voice_id: voice_id, content: 'Turn 1')
50
+ dialogue.add_turn!(voice_id: voice_id, content: 'Turn 2')
51
+ result = dialogue.add_turn!(voice_id: voice_id, content: 'Turn 3')
52
+ expect(result).to be false
53
+ end
54
+
55
+ it 'accepts position and strength' do
56
+ turn = dialogue.add_turn!(voice_id: voice_id, content: 'Oppose it', position: :oppose, strength: 0.8)
57
+ expect(turn.position).to eq(:oppose)
58
+ expect(turn.strength).to eq(0.8)
59
+ end
60
+ end
61
+
62
+ describe '#conclude!' do
63
+ it 'sets status to :concluded and stores summary' do
64
+ dialogue.conclude!('We should refactor')
65
+ expect(dialogue.status).to eq(:concluded)
66
+ expect(dialogue.conclusion).to eq('We should refactor')
67
+ end
68
+
69
+ it 'returns true on success' do
70
+ expect(dialogue.conclude!('done')).to be true
71
+ end
72
+
73
+ it 'returns false if already concluded' do
74
+ dialogue.conclude!('first')
75
+ expect(dialogue.conclude!('second')).to be false
76
+ end
77
+ end
78
+
79
+ describe '#deadlock!' do
80
+ it 'sets status to :deadlocked' do
81
+ dialogue.deadlock!
82
+ expect(dialogue.status).to eq(:deadlocked)
83
+ end
84
+
85
+ it 'returns true on success' do
86
+ expect(dialogue.deadlock!).to be true
87
+ end
88
+
89
+ it 'returns false if not active' do
90
+ dialogue.conclude!('done')
91
+ expect(dialogue.deadlock!).to be false
92
+ end
93
+ end
94
+
95
+ describe '#abandon!' do
96
+ it 'sets status to :abandoned' do
97
+ dialogue.abandon!
98
+ expect(dialogue.status).to eq(:abandoned)
99
+ end
100
+
101
+ it 'returns false if already closed' do
102
+ dialogue.deadlock!
103
+ expect(dialogue.abandon!).to be false
104
+ end
105
+ end
106
+
107
+ describe '#active?' do
108
+ it 'is true when status is :open' do
109
+ expect(dialogue.active?).to be true
110
+ end
111
+
112
+ it 'is false after conclusion' do
113
+ dialogue.conclude!('done')
114
+ expect(dialogue.active?).to be false
115
+ end
116
+ end
117
+
118
+ describe '#concluded?' do
119
+ it 'is false initially' do
120
+ expect(dialogue.concluded?).to be false
121
+ end
122
+
123
+ it 'is true after conclude!' do
124
+ dialogue.conclude!('done')
125
+ expect(dialogue.concluded?).to be true
126
+ end
127
+ end
128
+
129
+ describe '#voice_positions' do
130
+ it 'returns empty hash when no turns' do
131
+ expect(dialogue.voice_positions).to eq({})
132
+ end
133
+
134
+ it 'returns average strength per voice' do
135
+ dialogue.add_turn!(voice_id: 'v1', content: 'A', strength: 0.4)
136
+ dialogue.add_turn!(voice_id: 'v1', content: 'B', strength: 0.6)
137
+ positions = dialogue.voice_positions
138
+ expect(positions['v1']).to be_within(0.001).of(0.5)
139
+ end
140
+
141
+ it 'tracks multiple voices separately' do
142
+ dialogue.add_turn!(voice_id: 'v1', content: 'A', strength: 0.8)
143
+ dialogue.add_turn!(voice_id: 'v2', content: 'B', strength: 0.2)
144
+ positions = dialogue.voice_positions
145
+ expect(positions['v1']).to be > positions['v2']
146
+ end
147
+ end
148
+
149
+ describe '#consensus_score' do
150
+ it 'returns 1.0 when no turns' do
151
+ expect(dialogue.consensus_score).to eq(1.0)
152
+ end
153
+
154
+ it 'returns 0.5 when no support or oppose turns' do
155
+ dialogue.add_turn!(voice_id: voice_id, content: 'A question', position: :question)
156
+ expect(dialogue.consensus_score).to eq(0.5)
157
+ end
158
+
159
+ it 'returns high score when all turns support' do
160
+ dialogue.add_turn!(voice_id: voice_id, content: 'Support 1', position: :support, strength: 0.9)
161
+ dialogue.add_turn!(voice_id: voice_id, content: 'Support 2', position: :support, strength: 0.8)
162
+ expect(dialogue.consensus_score).to eq(1.0)
163
+ end
164
+
165
+ it 'returns ~0.5 when support and oppose are balanced' do
166
+ dialogue.add_turn!(voice_id: 'v1', content: 'Support', position: :support, strength: 0.5)
167
+ dialogue.add_turn!(voice_id: 'v2', content: 'Oppose', position: :oppose, strength: 0.5)
168
+ expect(dialogue.consensus_score).to be_within(0.01).of(0.5)
169
+ end
170
+ end
171
+
172
+ describe '#consensus_label' do
173
+ it 'returns a symbol from CONSENSUS_LABELS' do
174
+ valid = %i[unanimous agreement mixed disagreement conflict]
175
+ expect(valid).to include(dialogue.consensus_label)
176
+ end
177
+ end
178
+
179
+ describe '#to_h' do
180
+ it 'includes expected keys' do
181
+ h = dialogue.to_h
182
+ expect(h).to include(:id, :topic, :status, :conclusion, :turn_count, :consensus_score, :consensus_label, :created_at, :turns)
183
+ end
184
+
185
+ it 'turns is an array' do
186
+ dialogue.add_turn!(voice_id: voice_id, content: 'x')
187
+ expect(dialogue.to_h[:turns]).to be_an(Array)
188
+ expect(dialogue.to_h[:turns].size).to eq(1)
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Self::SelfTalk::Helpers::DialogueTurn do
4
+ let(:dialogue_id) { 'dlg-001' }
5
+ let(:voice_id) { 'voice-001' }
6
+
7
+ subject(:turn) do
8
+ described_class.new(dialogue_id: dialogue_id, voice_id: voice_id, content: 'This is a thought')
9
+ end
10
+
11
+ describe '#initialize' do
12
+ it 'assigns a UUID id' do
13
+ expect(turn.id).to match(/\A[0-9a-f-]{36}\z/)
14
+ end
15
+
16
+ it 'assigns dialogue_id' do
17
+ expect(turn.dialogue_id).to eq(dialogue_id)
18
+ end
19
+
20
+ it 'assigns voice_id' do
21
+ expect(turn.voice_id).to eq(voice_id)
22
+ end
23
+
24
+ it 'assigns content' do
25
+ expect(turn.content).to eq('This is a thought')
26
+ end
27
+
28
+ it 'defaults position to :clarify' do
29
+ expect(turn.position).to eq(:clarify)
30
+ end
31
+
32
+ it 'defaults strength to 0.5' do
33
+ expect(turn.strength).to eq(0.5)
34
+ end
35
+
36
+ it 'accepts valid position' do
37
+ t = described_class.new(dialogue_id: dialogue_id, voice_id: voice_id, content: 'x', position: :support)
38
+ expect(t.position).to eq(:support)
39
+ end
40
+
41
+ it 'falls back to :clarify for unknown position' do
42
+ t = described_class.new(dialogue_id: dialogue_id, voice_id: voice_id, content: 'x', position: :unknown_pos)
43
+ expect(t.position).to eq(:clarify)
44
+ end
45
+
46
+ it 'clamps strength above 1.0' do
47
+ t = described_class.new(dialogue_id: dialogue_id, voice_id: voice_id, content: 'x', strength: 2.0)
48
+ expect(t.strength).to eq(1.0)
49
+ end
50
+
51
+ it 'clamps strength below 0.0' do
52
+ t = described_class.new(dialogue_id: dialogue_id, voice_id: voice_id, content: 'x', strength: -1.0)
53
+ expect(t.strength).to eq(0.0)
54
+ end
55
+
56
+ it 'sets created_at' do
57
+ expect(turn.created_at).to be_a(Time)
58
+ end
59
+ end
60
+
61
+ describe 'POSITIONS' do
62
+ it 'includes the four valid positions' do
63
+ expect(described_class::POSITIONS).to include(:support, :oppose, :question, :clarify)
64
+ end
65
+ end
66
+
67
+ describe '#to_h' do
68
+ it 'returns a hash with expected keys' do
69
+ h = turn.to_h
70
+ expect(h).to include(:id, :dialogue_id, :voice_id, :content, :position, :strength, :created_at)
71
+ end
72
+
73
+ it 'rounds strength to 10 decimal places' do
74
+ h = turn.to_h
75
+ expect(h[:strength]).to be_a(Float)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Self::SelfTalk::Helpers::InnerVoice do
4
+ subject(:voice) { described_class.new(name: 'Critic', voice_type: :critic) }
5
+
6
+ describe '#initialize' do
7
+ it 'assigns a UUID id' do
8
+ expect(voice.id).to match(/\A[0-9a-f-]{36}\z/)
9
+ end
10
+
11
+ it 'assigns name' do
12
+ expect(voice.name).to eq('Critic')
13
+ end
14
+
15
+ it 'assigns voice_type' do
16
+ expect(voice.voice_type).to eq(:critic)
17
+ end
18
+
19
+ it 'defaults volume to DEFAULT_VOLUME' do
20
+ expect(voice.volume).to eq(0.5)
21
+ end
22
+
23
+ it 'accepts custom volume' do
24
+ v = described_class.new(name: 'Loud', voice_type: :encourager, volume: 0.9)
25
+ expect(v.volume).to eq(0.9)
26
+ end
27
+
28
+ it 'clamps volume above 1.0' do
29
+ v = described_class.new(name: 'Over', voice_type: :analyst, volume: 1.5)
30
+ expect(v.volume).to eq(1.0)
31
+ end
32
+
33
+ it 'clamps volume below 0.0' do
34
+ v = described_class.new(name: 'Under', voice_type: :rebel, volume: -0.5)
35
+ expect(v.volume).to eq(0.0)
36
+ end
37
+
38
+ it 'defaults active to true' do
39
+ expect(voice.active).to be true
40
+ end
41
+
42
+ it 'defaults bias_direction to nil' do
43
+ expect(voice.bias_direction).to be_nil
44
+ end
45
+
46
+ it 'accepts bias_direction' do
47
+ v = described_class.new(name: 'Biased', voice_type: :pragmatist, bias_direction: :conservative)
48
+ expect(v.bias_direction).to eq(:conservative)
49
+ end
50
+
51
+ it 'sets created_at' do
52
+ expect(voice.created_at).to be_a(Time)
53
+ end
54
+ end
55
+
56
+ describe '#amplify!' do
57
+ it 'increases volume by VOLUME_BOOST by default' do
58
+ before = voice.volume
59
+ voice.amplify!
60
+ expect(voice.volume).to be_within(0.001).of(before + 0.1)
61
+ end
62
+
63
+ it 'accepts custom amount' do
64
+ voice.amplify!(0.2)
65
+ expect(voice.volume).to be_within(0.001).of(0.7)
66
+ end
67
+
68
+ it 'clamps at 1.0' do
69
+ v = described_class.new(name: 'Max', voice_type: :visionary, volume: 0.95)
70
+ v.amplify!(0.2)
71
+ expect(v.volume).to eq(1.0)
72
+ end
73
+
74
+ it 'returns self for chaining' do
75
+ expect(voice.amplify!).to equal(voice)
76
+ end
77
+ end
78
+
79
+ describe '#dampen!' do
80
+ it 'decreases volume by VOLUME_DECAY by default' do
81
+ before = voice.volume
82
+ voice.dampen!
83
+ expect(voice.volume).to be_within(0.001).of(before - 0.05)
84
+ end
85
+
86
+ it 'accepts custom amount' do
87
+ voice.dampen!(0.3)
88
+ expect(voice.volume).to be_within(0.001).of(0.2)
89
+ end
90
+
91
+ it 'clamps at 0.0' do
92
+ v = described_class.new(name: 'Min', voice_type: :caretaker, volume: 0.02)
93
+ v.dampen!(0.1)
94
+ expect(v.volume).to eq(0.0)
95
+ end
96
+
97
+ it 'returns self for chaining' do
98
+ expect(voice.dampen!).to equal(voice)
99
+ end
100
+ end
101
+
102
+ describe '#mute!' do
103
+ it 'sets active to false' do
104
+ voice.mute!
105
+ expect(voice.active).to be false
106
+ end
107
+
108
+ it 'returns self for chaining' do
109
+ expect(voice.mute!).to equal(voice)
110
+ end
111
+ end
112
+
113
+ describe '#unmute!' do
114
+ it 'sets active to true after muting' do
115
+ voice.mute!
116
+ voice.unmute!
117
+ expect(voice.active).to be true
118
+ end
119
+
120
+ it 'returns self for chaining' do
121
+ expect(voice.unmute!).to equal(voice)
122
+ end
123
+ end
124
+
125
+ describe '#dominant?' do
126
+ it 'returns true when volume >= 0.7' do
127
+ v = described_class.new(name: 'Dom', voice_type: :analyst, volume: 0.7)
128
+ expect(v.dominant?).to be true
129
+ end
130
+
131
+ it 'returns false when volume < 0.7' do
132
+ v = described_class.new(name: 'Not', voice_type: :analyst, volume: 0.69)
133
+ expect(v.dominant?).to be false
134
+ end
135
+ end
136
+
137
+ describe '#quiet?' do
138
+ it 'returns true when volume <= 0.3' do
139
+ v = described_class.new(name: 'Quiet', voice_type: :caretaker, volume: 0.3)
140
+ expect(v.quiet?).to be true
141
+ end
142
+
143
+ it 'returns false when volume > 0.3' do
144
+ v = described_class.new(name: 'NotQuiet', voice_type: :caretaker, volume: 0.31)
145
+ expect(v.quiet?).to be false
146
+ end
147
+ end
148
+
149
+ describe '#volume_label' do
150
+ it 'returns :commanding for loud voices' do
151
+ v = described_class.new(name: 'L', voice_type: :rebel, volume: 0.9)
152
+ expect(v.volume_label).to eq(:commanding)
153
+ end
154
+
155
+ it 'returns :silent for muted-volume voices' do
156
+ v = described_class.new(name: 'S', voice_type: :rebel, volume: 0.05)
157
+ expect(v.volume_label).to eq(:silent)
158
+ end
159
+ end
160
+
161
+ describe '#to_h' do
162
+ it 'returns a hash with expected keys' do
163
+ h = voice.to_h
164
+ expect(h).to include(:id, :name, :voice_type, :volume, :volume_label, :bias_direction, :active, :dominant, :quiet, :created_at)
165
+ end
166
+
167
+ it 'rounds volume to 10 decimal places' do
168
+ h = voice.to_h
169
+ expect(h[:volume]).to be_a(Float)
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Agentic::Self::SelfTalk::Helpers::LlmEnhancer do
4
+ describe '.available?' do
5
+ context 'when Legion::LLM is not defined' do
6
+ it 'returns false' do
7
+ hide_const('Legion::LLM')
8
+ expect(described_class.available?).to be false
9
+ end
10
+ end
11
+
12
+ context 'when Legion::LLM is defined but not started' do
13
+ it 'returns false' do
14
+ llm_double = double('Legion::LLM', started?: false)
15
+ stub_const('Legion::LLM', llm_double)
16
+ expect(described_class.available?).to be false
17
+ end
18
+ end
19
+
20
+ context 'when Legion::LLM is started' do
21
+ it 'returns true' do
22
+ llm_double = double('Legion::LLM', started?: true)
23
+ stub_const('Legion::LLM', llm_double)
24
+ expect(described_class.available?).to be true
25
+ end
26
+ end
27
+
28
+ context 'when Legion::LLM raises an error' do
29
+ it 'returns false' do
30
+ llm_double = double('Legion::LLM')
31
+ allow(llm_double).to receive(:respond_to?).and_raise(StandardError)
32
+ stub_const('Legion::LLM', llm_double)
33
+ expect(described_class.available?).to be false
34
+ end
35
+ end
36
+ end
37
+
38
+ describe '.generate_turn' do
39
+ let(:prior_turns) do
40
+ [
41
+ { voice_name: 'Analyst', voice_id: 'uuid-1', position: :support, content: 'The data supports this.' },
42
+ { voice_name: 'Critic', voice_id: 'uuid-2', position: :oppose, content: 'There are serious risks.' }
43
+ ]
44
+ end
45
+
46
+ context 'when LLM returns a valid formatted response' do
47
+ it 'returns content and position hash' do
48
+ response_double = double('response', content: "POSITION: oppose\nCONTENT: The risks outweigh the benefits here.")
49
+ chat_double = double('chat')
50
+ allow(chat_double).to receive(:with_instructions)
51
+ allow(chat_double).to receive(:ask).and_return(response_double)
52
+ llm_double = double('Legion::LLM', started?: true)
53
+ allow(llm_double).to receive(:chat).and_return(chat_double)
54
+ stub_const('Legion::LLM', llm_double)
55
+
56
+ result = described_class.generate_turn(
57
+ voice_type: :critic,
58
+ topic: 'Should we proceed with the plan?',
59
+ prior_turns: prior_turns
60
+ )
61
+
62
+ expect(result).to be_a(Hash)
63
+ expect(result[:content]).to eq('The risks outweigh the benefits here.')
64
+ expect(result[:position]).to eq(:oppose)
65
+ end
66
+ end
67
+
68
+ context 'when LLM returns a support position' do
69
+ it 'parses support correctly' do
70
+ response_double = double('response', content: "POSITION: support\nCONTENT: I think we should go forward.")
71
+ chat_double = double('chat')
72
+ allow(chat_double).to receive(:with_instructions)
73
+ allow(chat_double).to receive(:ask).and_return(response_double)
74
+ llm_double = double('Legion::LLM', started?: true)
75
+ allow(llm_double).to receive(:chat).and_return(chat_double)
76
+ stub_const('Legion::LLM', llm_double)
77
+
78
+ result = described_class.generate_turn(
79
+ voice_type: :encourager,
80
+ topic: 'Should we proceed?',
81
+ prior_turns: []
82
+ )
83
+
84
+ expect(result[:position]).to eq(:support)
85
+ end
86
+ end
87
+
88
+ context 'when LLM returns unparseable response' do
89
+ it 'returns nil' do
90
+ response_double = double('response', content: 'This is not formatted correctly.')
91
+ chat_double = double('chat')
92
+ allow(chat_double).to receive(:with_instructions)
93
+ allow(chat_double).to receive(:ask).and_return(response_double)
94
+ llm_double = double('Legion::LLM', started?: true)
95
+ allow(llm_double).to receive(:chat).and_return(chat_double)
96
+ stub_const('Legion::LLM', llm_double)
97
+
98
+ result = described_class.generate_turn(
99
+ voice_type: :analyst,
100
+ topic: 'test',
101
+ prior_turns: []
102
+ )
103
+
104
+ expect(result).to be_nil
105
+ end
106
+ end
107
+
108
+ context 'when LLM returns nil' do
109
+ it 'returns nil' do
110
+ chat_double = double('chat')
111
+ allow(chat_double).to receive(:with_instructions)
112
+ allow(chat_double).to receive(:ask).and_return(nil)
113
+ llm_double = double('Legion::LLM', started?: true)
114
+ allow(llm_double).to receive(:chat).and_return(chat_double)
115
+ stub_const('Legion::LLM', llm_double)
116
+
117
+ result = described_class.generate_turn(voice_type: :analyst, topic: 'test', prior_turns: [])
118
+ expect(result).to be_nil
119
+ end
120
+ end
121
+
122
+ context 'when an error occurs' do
123
+ it 'returns nil and logs a warning' do
124
+ llm_double = double('Legion::LLM', started?: true)
125
+ allow(llm_double).to receive(:chat).and_raise(StandardError, 'timeout')
126
+ stub_const('Legion::LLM', llm_double)
127
+
128
+ expect(Legion::Logging).to receive(:warn).with(/self_talk:llm.*generate_turn failed/)
129
+ result = described_class.generate_turn(voice_type: :critic, topic: 'test', prior_turns: [])
130
+ expect(result).to be_nil
131
+ end
132
+ end
133
+ end
134
+
135
+ describe '.summarize_dialogue' do
136
+ let(:turns) do
137
+ [
138
+ { voice_name: 'Analyst', voice_id: 'uuid-1', position: :support, content: 'Evidence is strong.' },
139
+ { voice_name: 'Critic', voice_id: 'uuid-2', position: :oppose, content: 'Too risky.' },
140
+ { voice_name: 'Pragmatist', voice_id: 'uuid-3', position: :clarify, content: 'Let us scope this down.' }
141
+ ]
142
+ end
143
+
144
+ context 'when LLM returns a valid formatted response' do
145
+ it 'returns summary and recommendation hash' do
146
+ response_double = double('response',
147
+ content: "RECOMMENDATION: abstain\nSUMMARY: The dialogue was split. A smaller scope was suggested.")
148
+ chat_double = double('chat')
149
+ allow(chat_double).to receive(:with_instructions)
150
+ allow(chat_double).to receive(:ask).and_return(response_double)
151
+ llm_double = double('Legion::LLM', started?: true)
152
+ allow(llm_double).to receive(:chat).and_return(chat_double)
153
+ stub_const('Legion::LLM', llm_double)
154
+
155
+ result = described_class.summarize_dialogue(topic: 'Should we proceed?', turns: turns)
156
+
157
+ expect(result).to be_a(Hash)
158
+ expect(result[:recommendation]).to eq(:abstain)
159
+ expect(result[:summary]).to include('smaller scope')
160
+ end
161
+ end
162
+
163
+ context 'when LLM returns a support recommendation' do
164
+ it 'parses support correctly' do
165
+ response_double = double('response',
166
+ content: "RECOMMENDATION: support\nSUMMARY: The group agreed to move forward.")
167
+ chat_double = double('chat')
168
+ allow(chat_double).to receive(:with_instructions)
169
+ allow(chat_double).to receive(:ask).and_return(response_double)
170
+ llm_double = double('Legion::LLM', started?: true)
171
+ allow(llm_double).to receive(:chat).and_return(chat_double)
172
+ stub_const('Legion::LLM', llm_double)
173
+
174
+ result = described_class.summarize_dialogue(topic: 'test', turns: [])
175
+ expect(result[:recommendation]).to eq(:support)
176
+ end
177
+ end
178
+
179
+ context 'when LLM returns unparseable response' do
180
+ it 'returns nil' do
181
+ response_double = double('response', content: 'No clear structure here.')
182
+ chat_double = double('chat')
183
+ allow(chat_double).to receive(:with_instructions)
184
+ allow(chat_double).to receive(:ask).and_return(response_double)
185
+ llm_double = double('Legion::LLM', started?: true)
186
+ allow(llm_double).to receive(:chat).and_return(chat_double)
187
+ stub_const('Legion::LLM', llm_double)
188
+
189
+ result = described_class.summarize_dialogue(topic: 'test', turns: [])
190
+ expect(result).to be_nil
191
+ end
192
+ end
193
+
194
+ context 'when an error occurs' do
195
+ it 'returns nil and logs a warning' do
196
+ llm_double = double('Legion::LLM', started?: true)
197
+ allow(llm_double).to receive(:chat).and_raise(StandardError, 'network error')
198
+ stub_const('Legion::LLM', llm_double)
199
+
200
+ expect(Legion::Logging).to receive(:warn).with(/self_talk:llm.*summarize_dialogue failed/)
201
+ result = described_class.summarize_dialogue(topic: 'test', turns: [])
202
+ expect(result).to be_nil
203
+ end
204
+ end
205
+ end
206
+ end