prompt_objects 0.6.0 → 0.7.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 (291) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +126 -0
  3. data/README.md +93 -22
  4. data/exe/poop +5 -0
  5. data/exe/prompt_objects +1 -1108
  6. data/exe/prompt_objects_mcp +5 -3
  7. data/lib/prompt_objects/capability.rb +33 -0
  8. data/lib/prompt_objects/cli/diagnostics_command.rb +69 -0
  9. data/lib/prompt_objects/cli/env_command.rb +435 -0
  10. data/lib/prompt_objects/cli/events_command.rb +145 -0
  11. data/lib/prompt_objects/cli/explore_command.rb +155 -0
  12. data/lib/prompt_objects/cli/helpers.rb +91 -0
  13. data/lib/prompt_objects/cli/message_command.rb +216 -0
  14. data/lib/prompt_objects/cli/repl_command.rb +366 -0
  15. data/lib/prompt_objects/cli/serve_command.rb +162 -0
  16. data/lib/prompt_objects/cli.rb +64 -422
  17. data/lib/prompt_objects/connectors/base.rb +16 -29
  18. data/lib/prompt_objects/connectors/mcp.rb +82 -98
  19. data/lib/prompt_objects/domain/records.rb +45 -0
  20. data/lib/prompt_objects/environment/exporter.rb +5 -4
  21. data/lib/prompt_objects/environment/git.rb +21 -1
  22. data/lib/prompt_objects/environment/importer.rb +5 -4
  23. data/lib/prompt_objects/environment/manager.rb +44 -6
  24. data/lib/prompt_objects/environment/manifest.rb +26 -4
  25. data/lib/prompt_objects/environment.rb +1337 -20
  26. data/lib/prompt_objects/execution/blocking_executor.rb +88 -0
  27. data/lib/prompt_objects/execution/context.rb +137 -0
  28. data/lib/prompt_objects/execution/contracts.rb +59 -0
  29. data/lib/prompt_objects/execution/permit_pool.rb +49 -0
  30. data/lib/prompt_objects/execution/policy.rb +47 -0
  31. data/lib/prompt_objects/execution/resource_lock_manager.rb +63 -0
  32. data/lib/prompt_objects/execution/runner.rb +498 -0
  33. data/lib/prompt_objects/execution/scheduler.rb +325 -0
  34. data/lib/prompt_objects/llm/factory.rb +4 -4
  35. data/lib/prompt_objects/llm/gemini_adapter.rb +25 -8
  36. data/lib/prompt_objects/llm/openai_adapter.rb +9 -1
  37. data/lib/prompt_objects/llm/pricing.rb +9 -2
  38. data/lib/prompt_objects/llm/response.rb +9 -4
  39. data/lib/prompt_objects/loader.rb +28 -1
  40. data/lib/prompt_objects/mcp/server.rb +6 -3
  41. data/lib/prompt_objects/mcp/tools/get_conversation.rb +15 -11
  42. data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +2 -2
  43. data/lib/prompt_objects/mcp/tools/respond_to_request.rb +3 -3
  44. data/lib/prompt_objects/mcp/tools/send_message.rb +42 -32
  45. data/lib/prompt_objects/operations/diagnostics.rb +115 -0
  46. data/lib/prompt_objects/operations/retention.rb +75 -0
  47. data/lib/prompt_objects/operations/run_trace_builder.rb +152 -0
  48. data/lib/prompt_objects/primitives/http_get.rb +2 -0
  49. data/lib/prompt_objects/primitives/list_files.rb +2 -0
  50. data/lib/prompt_objects/primitives/read_file.rb +2 -0
  51. data/lib/prompt_objects/primitives/think.rb +36 -0
  52. data/lib/prompt_objects/primitives/write_file.rb +9 -0
  53. data/lib/prompt_objects/prompt_object.rb +171 -519
  54. data/lib/prompt_objects/repositories/artifact_repository.rb +109 -0
  55. data/lib/prompt_objects/repositories/base_repository.rb +37 -0
  56. data/lib/prompt_objects/repositories/definition_repository.rb +36 -0
  57. data/lib/prompt_objects/repositories/env_data_repository.rb +103 -0
  58. data/lib/prompt_objects/repositories/event_repository.rb +105 -0
  59. data/lib/prompt_objects/repositories/message_repository.rb +73 -0
  60. data/lib/prompt_objects/repositories/run_repository.rb +254 -0
  61. data/lib/prompt_objects/repositories/thread_repository.rb +87 -0
  62. data/lib/prompt_objects/repositories/tool_execution_repository.rb +88 -0
  63. data/lib/prompt_objects/repositories/workspace_session_repository.rb +61 -0
  64. data/lib/prompt_objects/server/api/routes.rb +549 -53
  65. data/lib/prompt_objects/server/app.rb +133 -14
  66. data/lib/prompt_objects/server/file_watcher.rb +9 -0
  67. data/lib/prompt_objects/server/public/assets/index-BjKlODDD.js +90 -0
  68. data/lib/prompt_objects/server/public/assets/index-w3fpIb5c.css +1 -0
  69. data/lib/prompt_objects/server/public/index.html +2 -2
  70. data/lib/prompt_objects/server/websocket_handler.rb +202 -227
  71. data/lib/prompt_objects/server.rb +170 -1
  72. data/lib/prompt_objects/service.rb +129 -0
  73. data/lib/prompt_objects/session/store.rb +600 -13
  74. data/lib/prompt_objects/universal/add_capability.rb +1 -1
  75. data/lib/prompt_objects/universal/create_capability.rb +4 -2
  76. data/lib/prompt_objects/universal/delete_env_data.rb +23 -15
  77. data/lib/prompt_objects/universal/delete_primitive.rb +1 -1
  78. data/lib/prompt_objects/universal/dispatch_po.rb +54 -0
  79. data/lib/prompt_objects/universal/get_env_data.rb +8 -14
  80. data/lib/prompt_objects/universal/get_run_status.rb +30 -0
  81. data/lib/prompt_objects/universal/join_runs.rb +35 -0
  82. data/lib/prompt_objects/universal/list_env_data.rb +10 -14
  83. data/lib/prompt_objects/universal/modify_primitive.rb +1 -1
  84. data/lib/prompt_objects/universal/render_artifact.rb +91 -0
  85. data/lib/prompt_objects/universal/request_primitive.rb +1 -1
  86. data/lib/prompt_objects/universal/{store_env_data.rb → set_env_data.rb} +34 -20
  87. data/lib/prompt_objects/version.rb +5 -0
  88. data/lib/prompt_objects.rb +33 -8
  89. data/objects/coordinator.md +8 -4
  90. data/templates/arc-agi-1/objects/data_manager.md +2 -2
  91. data/templates/arc-agi-1/objects/observer.md +2 -2
  92. data/templates/arc-agi-1/objects/solver.md +6 -5
  93. data/templates/arc-agi-1/objects/verifier.md +2 -2
  94. data/templates/arc-agi-3/manifest.yml +19 -0
  95. data/templates/arc-agi-3/objects/analyst.md +79 -0
  96. data/templates/arc-agi-3/objects/player.md +76 -0
  97. data/templates/arc-agi-3/objects/strategist.md +97 -0
  98. data/templates/arc-agi-3/primitives/action_log.rb +133 -0
  99. data/templates/arc-agi-3/primitives/arc3_api.rb +223 -0
  100. data/templates/arc-agi-3/primitives/arc3_scorecard.rb +131 -0
  101. data/templates/arc-agi-3/primitives/frame_diff.rb +143 -0
  102. data/templates/arc-agi-3/primitives/frame_info.rb +96 -0
  103. data/templates/arc-agi-3/primitives/render_frame.rb +140 -0
  104. data/templates/basic/objects/basic.md +11 -3
  105. data/templates/chiptune/extras/sonic_pi_boot.rb +220 -0
  106. data/templates/chiptune/manifest.yml +20 -0
  107. data/templates/chiptune/objects/conductor.md +184 -0
  108. data/templates/chiptune/objects/sonic_pi.md +65 -0
  109. data/templates/chiptune/objects/voice_bass.md +44 -0
  110. data/templates/chiptune/objects/voice_lead.md +44 -0
  111. data/templates/chiptune/objects/voice_pad.md +43 -0
  112. data/templates/chiptune/objects/voice_perc.md +45 -0
  113. data/templates/chiptune/primitives/play_note.rb +59 -0
  114. data/templates/chiptune/primitives/play_sequence.rb +55 -0
  115. data/templates/chiptune/primitives/query_sonic_pi.rb +141 -0
  116. data/templates/chiptune/primitives/set_synth.rb +57 -0
  117. data/templates/chiptune/primitives/set_tempo.rb +45 -0
  118. data/templates/chiptune/primitives/stop_voice.rb +46 -0
  119. data/templates/chiptune/primitives/support/osc_bridge.rb +33 -0
  120. data/templates/chiptune/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  121. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  122. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  123. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  124. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  125. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  126. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  127. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  128. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  129. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  130. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  131. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  132. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  133. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  134. data/templates/developer/objects/code_reviewer.md +1 -1
  135. data/templates/developer/objects/coordinator.md +16 -3
  136. data/templates/developer/objects/debugger.md +3 -3
  137. data/templates/empty/objects/assistant.md +10 -5
  138. data/templates/minimal/objects/assistant.md +10 -5
  139. data/templates/music/extras/sonic_pi_boot.rb +220 -0
  140. data/templates/music/manifest.yml +16 -0
  141. data/templates/music/objects/conductor.md +154 -0
  142. data/templates/music/objects/sonic_pi.md +65 -0
  143. data/templates/music/primitives/play_note.rb +55 -0
  144. data/templates/music/primitives/play_sequence.rb +51 -0
  145. data/templates/music/primitives/query_sonic_pi.rb +139 -0
  146. data/templates/music/primitives/set_synth.rb +53 -0
  147. data/templates/music/primitives/set_tempo.rb +43 -0
  148. data/templates/music/primitives/stop_voice.rb +44 -0
  149. data/templates/music/primitives/support/osc_bridge.rb +33 -0
  150. data/templates/music/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  151. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  152. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  153. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  154. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  155. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  156. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  157. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  158. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  159. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  160. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  161. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  162. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  163. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  164. data/templates/music-beginner/README.md +100 -0
  165. data/templates/music-beginner/extras/README.md +27 -0
  166. data/templates/music-beginner/extras/sonic_pi_boot.rb +297 -0
  167. data/templates/music-beginner/manifest.yml +25 -0
  168. data/templates/music-beginner/objects/conductor.md +222 -0
  169. data/templates/music-beginner/objects/engine_setup.md +54 -0
  170. data/templates/music-beginner/objects/voice_bass.md +43 -0
  171. data/templates/music-beginner/objects/voice_keys.md +48 -0
  172. data/templates/music-beginner/objects/voice_pad.md +46 -0
  173. data/templates/music-beginner/objects/voice_percussion.md +45 -0
  174. data/templates/music-beginner/objects/voice_texture.md +37 -0
  175. data/templates/music-beginner/primitives/play_pattern.rb +118 -0
  176. data/templates/music-beginner/primitives/query_music_engine.rb +94 -0
  177. data/templates/music-beginner/primitives/set_instrument.rb +47 -0
  178. data/templates/music-beginner/primitives/set_tempo.rb +43 -0
  179. data/templates/music-beginner/primitives/set_voice_level.rb +46 -0
  180. data/templates/music-beginner/primitives/stop_voice.rb +37 -0
  181. data/templates/music-beginner/primitives/support/osc_bridge.rb +25 -0
  182. data/templates/music-beginner/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  183. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  184. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  185. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  186. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  187. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  188. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  189. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  190. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  191. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  192. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  193. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  194. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  195. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  196. data/templates/music-pro/extras/AUDITION.md +100 -0
  197. data/templates/music-pro/extras/sonic_pi_boot.rb +546 -0
  198. data/templates/music-pro/manifest.yml +19 -0
  199. data/templates/music-pro/objects/conductor.md +126 -0
  200. data/templates/music-pro/objects/sonic_pi.md +35 -0
  201. data/templates/music-pro/objects/voice_harmony.md +48 -0
  202. data/templates/music-pro/objects/voice_rhythm.md +43 -0
  203. data/templates/music-pro/objects/voice_texture.md +42 -0
  204. data/templates/music-pro/primitives/define_patch.rb +46 -0
  205. data/templates/music-pro/primitives/query_music_engine.rb +69 -0
  206. data/templates/music-pro/primitives/set_live_pattern.rb +70 -0
  207. data/templates/music-pro/primitives/set_tempo.rb +31 -0
  208. data/templates/music-pro/primitives/stop_voice.rb +31 -0
  209. data/templates/music-pro/primitives/support/music_protocol.rb +136 -0
  210. data/templates/music-pro/primitives/support/osc_bridge.rb +33 -0
  211. data/templates/music-pro/primitives/trigger_event.rb +44 -0
  212. data/templates/music-pro/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  213. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  214. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  215. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  216. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  217. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  218. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  219. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  220. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  221. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  222. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  223. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  224. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  225. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  226. data/templates/writer/objects/coordinator.md +14 -3
  227. data/templates/writer/objects/editor.md +1 -1
  228. data/templates/writer/objects/researcher.md +2 -2
  229. data/tools/thread-explorer.html +1 -1
  230. metadata +172 -65
  231. data/CLAUDE.md +0 -177
  232. data/Gemfile +0 -10
  233. data/Rakefile +0 -27
  234. data/frontend/.gitignore +0 -3
  235. data/frontend/index.html +0 -17
  236. data/frontend/package-lock.json +0 -4540
  237. data/frontend/package.json +0 -36
  238. data/frontend/postcss.config.js +0 -6
  239. data/frontend/src/App.tsx +0 -104
  240. data/frontend/src/canvas/CanvasView.tsx +0 -113
  241. data/frontend/src/canvas/ForceLayout.ts +0 -115
  242. data/frontend/src/canvas/SceneManager.ts +0 -587
  243. data/frontend/src/canvas/canvasStore.ts +0 -47
  244. data/frontend/src/canvas/constants.ts +0 -95
  245. data/frontend/src/canvas/controls/CameraControls.ts +0 -98
  246. data/frontend/src/canvas/edges/MessageArc.ts +0 -149
  247. data/frontend/src/canvas/inspector/InspectorPanel.tsx +0 -31
  248. data/frontend/src/canvas/inspector/POInspector.tsx +0 -262
  249. data/frontend/src/canvas/inspector/ToolCallInspector.tsx +0 -67
  250. data/frontend/src/canvas/nodes/PONode.ts +0 -249
  251. data/frontend/src/canvas/nodes/ToolCallNode.ts +0 -116
  252. data/frontend/src/canvas/types.ts +0 -24
  253. data/frontend/src/components/ContextMenu.tsx +0 -68
  254. data/frontend/src/components/EnvDataPane.tsx +0 -69
  255. data/frontend/src/components/Inspector.tsx +0 -263
  256. data/frontend/src/components/MarkdownMessage.tsx +0 -155
  257. data/frontend/src/components/MethodList.tsx +0 -90
  258. data/frontend/src/components/ModelSelector.tsx +0 -115
  259. data/frontend/src/components/NotificationPanel.tsx +0 -130
  260. data/frontend/src/components/ObjectList.tsx +0 -78
  261. data/frontend/src/components/PaneSlot.tsx +0 -30
  262. data/frontend/src/components/SourcePane.tsx +0 -202
  263. data/frontend/src/components/SystemBar.tsx +0 -74
  264. data/frontend/src/components/Transcript.tsx +0 -76
  265. data/frontend/src/components/UsagePanel.tsx +0 -105
  266. data/frontend/src/components/Workspace.tsx +0 -260
  267. data/frontend/src/components/index.ts +0 -12
  268. data/frontend/src/hooks/useResize.ts +0 -55
  269. data/frontend/src/hooks/useWebSocket.ts +0 -559
  270. data/frontend/src/index.css +0 -102
  271. data/frontend/src/main.tsx +0 -10
  272. data/frontend/src/store/index.ts +0 -311
  273. data/frontend/src/types/index.ts +0 -176
  274. data/frontend/tailwind.config.js +0 -44
  275. data/frontend/tsconfig.json +0 -30
  276. data/frontend/vite.config.ts +0 -29
  277. data/lib/prompt_objects/server/public/assets/index-DEPawnfZ.js +0 -4345
  278. data/lib/prompt_objects/server/public/assets/index-oMrRce1m.css +0 -1
  279. data/lib/prompt_objects/universal/add_primitive.rb +0 -118
  280. data/lib/prompt_objects/universal/ask_human.rb +0 -109
  281. data/lib/prompt_objects/universal/create_primitive.rb +0 -174
  282. data/lib/prompt_objects/universal/list_primitives.rb +0 -145
  283. data/lib/prompt_objects/universal/think.rb +0 -41
  284. data/lib/prompt_objects/universal/update_env_data.rb +0 -88
  285. data/prompt_objects.gemspec +0 -50
  286. data/templates/basic/.gitignore +0 -2
  287. data/templates/developer/.gitignore +0 -5
  288. data/templates/empty/.gitignore +0 -5
  289. data/templates/empty/objects/.gitkeep +0 -0
  290. data/templates/minimal/.gitignore +0 -5
  291. data/templates/writer/.gitignore +0 -5
@@ -27,9 +27,17 @@ module PromptObjects
27
27
  puts ""
28
28
  puts "Prompt Objects:"
29
29
  runtime.registry.prompt_objects.each do |po|
30
- puts " - #{po.name}: #{po.description}"
30
+ auto = po.autonomous? ? " (autonomous: #{po.autonomous_config['interval']}s)" : ""
31
+ puts " - #{po.name}: #{po.description}#{auto}"
31
32
  end
32
33
  puts ""
34
+ if runtime.services.any?
35
+ puts "Services:"
36
+ runtime.services.each do |s|
37
+ puts " - #{s.name}: #{s.description || '(no description)'}"
38
+ end
39
+ puts ""
40
+ end
33
41
  puts "Press Ctrl+C to stop"
34
42
  puts ""
35
43
 
@@ -59,6 +67,9 @@ module PromptObjects
59
67
  puts "Broadcast: PO modified (programmatic) - #{po.name}"
60
68
  }
61
69
 
70
+ # Register broadcast callback for services to push WebSocket messages
71
+ runtime.on_broadcast = ->(message) { app.broadcast(message) }
72
+
62
73
  # Start file watcher for live updates (manual file edits)
63
74
  file_watcher = nil
64
75
  if env_path
@@ -69,6 +80,12 @@ module PromptObjects
69
80
  file_watcher.start
70
81
  end
71
82
 
83
+ # Start environment services (calls service.start, no scheduling yet)
84
+ runtime.start_services if runtime.services.any?
85
+
86
+ # Start environment connectors (Discord/Slack/webhook bridges)
87
+ runtime.start_connectors if runtime.connectors.any?
88
+
72
89
  # Write .server file for CLI discovery
73
90
  server_file = write_server_file(env_path, host: host, port: port) if env_path
74
91
 
@@ -80,15 +97,167 @@ module PromptObjects
80
97
  endpoint
81
98
  )
82
99
 
100
+ # Schedule service tick loops as Async tasks.
101
+ # These cooperate with Falcon's event loop instead of fighting it.
102
+ schedule_service_ticks(task, runtime)
103
+
104
+ # Schedule autonomous PO execution as Async tasks.
105
+ schedule_autonomous_pos(task, runtime, app)
106
+
83
107
  task.async do
84
108
  server.run
85
109
  end
86
110
  ensure
111
+ runtime.stop_connectors if runtime.connectors.any?
112
+ runtime.stop_services if runtime.services.any?
113
+ runtime.shutdown_execution(wait: false)
87
114
  file_watcher&.stop
115
+ app.close
88
116
  remove_server_file(server_file) if server_file
89
117
  end
90
118
  end
91
119
 
120
+ # Schedule Async tick tasks for services that have an interval.
121
+ # Runs inside the Async event loop so ticks cooperate with Falcon.
122
+ # @param task [Async::Task] The parent Async task
123
+ # @param runtime [Runtime] The environment runtime
124
+ def self.schedule_service_ticks(task, runtime)
125
+ runtime.services.each do |service|
126
+ next unless service.interval
127
+
128
+ task.async do |subtask|
129
+ puts "Service tick loop started: #{service.name} (every #{service.interval}s)"
130
+ loop do
131
+ subtask.sleep service.interval
132
+ service.tick
133
+ rescue => e
134
+ warn "Service #{service.name} tick error: #{e.class}: #{e.message}"
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ # Schedule Async tasks for autonomous POs that think on their own interval.
141
+ # Uses runtime.inject_message — the same code path as human messages,
142
+ # service events, and webhooks.
143
+ # @param task [Async::Task] The parent Async task
144
+ # @param runtime [Runtime] The environment runtime
145
+ # @param app [App] The server app (unused — broadcasting goes through runtime)
146
+ def self.schedule_autonomous_pos(task, runtime, app)
147
+ runtime.registry.prompt_objects.each do |po|
148
+ next unless po.autonomous?
149
+
150
+ auto_config = po.autonomous_config
151
+ next unless auto_config["start_on_boot"]
152
+
153
+ po_name = po.name
154
+
155
+ task.async do |subtask|
156
+ puts "Autonomous PO scheduled: #{po_name} (every #{auto_config['interval']}s, " \
157
+ "initial delay #{auto_config['initial_delay']}s)"
158
+
159
+ waiting_state = nil
160
+ loop do
161
+ current = current_autonomous_config(runtime, po_name)
162
+ break unless current && current["wait_for_human"] && !runtime.human_message_received?(po_name)
163
+
164
+ current_waiting_state = [current["interval"], current["initial_delay"]]
165
+ if current_waiting_state != waiting_state
166
+ runtime.update_autonomous_schedule(
167
+ po_name,
168
+ status: "waiting_for_human",
169
+ interval: current["interval"],
170
+ initial_delay: current["initial_delay"],
171
+ wait_for_human: true,
172
+ next_run_at: nil
173
+ )
174
+ waiting_state = current_waiting_state
175
+ end
176
+ subtask.sleep 0.1
177
+ end
178
+
179
+ next unless wait_for_live_autonomous_delay(
180
+ subtask, runtime, po_name, duration_key: "initial_delay"
181
+ )
182
+
183
+ loop do
184
+ current = current_autonomous_config(runtime, po_name)
185
+ break unless current
186
+
187
+ begin
188
+ runtime.update_autonomous_schedule(
189
+ po_name,
190
+ status: "running",
191
+ next_run_at: nil
192
+ )
193
+ # Autonomous work uses its own coalesced, summary-retention thread.
194
+ execution = lambda do
195
+ runtime.inject_message(
196
+ target_po: po_name,
197
+ content: current["message"].strip,
198
+ from_source: "timer:#{po_name}"
199
+ )
200
+ end
201
+ if app.respond_to?(:blocking_executor)
202
+ app.blocking_executor.call(&execution)
203
+ else
204
+ execution.call
205
+ end
206
+ rescue => e
207
+ warn "Autonomous PO #{po_name} error: #{e.class}: #{e.message}"
208
+ end
209
+
210
+ break unless wait_for_live_autonomous_delay(
211
+ subtask, runtime, po_name, duration_key: "interval"
212
+ )
213
+ end
214
+ end
215
+ end
216
+ end
217
+
218
+ # Sleep until the PO's current autonomous deadline while re-reading its
219
+ # frontmatter. In-app and external source edits can therefore change the
220
+ # message used by the next tick and move an interval deadline without a
221
+ # server restart.
222
+ def self.wait_for_live_autonomous_delay(subtask, runtime, po_name, duration_key:)
223
+ started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
224
+ published_deadline = nil
225
+
226
+ loop do
227
+ config = current_autonomous_config(runtime, po_name)
228
+ return false unless config
229
+
230
+ duration = [config[duration_key].to_f, 0].max
231
+ elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
232
+ remaining = duration - elapsed
233
+ return true if remaining <= 0
234
+
235
+ deadline = Time.now.utc + remaining
236
+ deadline_key = [duration, config["interval"], config["initial_delay"], config["wait_for_human"]]
237
+ if deadline_key != published_deadline
238
+ runtime.update_autonomous_schedule(
239
+ po_name,
240
+ status: "scheduled",
241
+ interval: config["interval"],
242
+ initial_delay: config["initial_delay"],
243
+ wait_for_human: config["wait_for_human"],
244
+ next_run_at: deadline.iso8601(6)
245
+ )
246
+ published_deadline = deadline_key
247
+ end
248
+
249
+ subtask.sleep([remaining, 0.1].min)
250
+ end
251
+ end
252
+
253
+ def self.current_autonomous_config(runtime, po_name)
254
+ po = runtime.registry.get(po_name)
255
+ return nil unless po.is_a?(PromptObject) && po.autonomous?
256
+
257
+ config = po.autonomous_config
258
+ config if config["start_on_boot"]
259
+ end
260
+
92
261
  # Write a .server file so CLI clients can discover the running server.
93
262
  # @param env_path [String] Path to the environment directory
94
263
  # @param host [String] Server host
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptObjects
4
+ # Base class for environment background services.
5
+ # Services run alongside the server and can publish to the message bus,
6
+ # write to env_data, and perform periodic background work.
7
+ #
8
+ # Subclasses live in an environment's services/ directory and are loaded
9
+ # automatically when the server starts.
10
+ #
11
+ # Example:
12
+ # module PromptObjects
13
+ # module Services
14
+ # class MyHeartbeat < PromptObjects::Service
15
+ # def description = "Polls external API every 10s"
16
+ # def interval = 10
17
+ # def tick
18
+ # @runtime.bus.publish(from: "service:#{name}", to: "broadcast", message: "heartbeat")
19
+ # end
20
+ # end
21
+ # end
22
+ # end
23
+ # Namespace for custom environment services.
24
+ module Services; end
25
+
26
+ class Service
27
+ attr_reader :name, :description
28
+
29
+ def initialize(runtime:)
30
+ @runtime = runtime
31
+ @name = self.class.name.split("::").last
32
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
33
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
34
+ .downcase
35
+ @description = nil
36
+ end
37
+
38
+ # Called when the server starts. Override to initialize resources.
39
+ def start; end
40
+
41
+ # Called when the server stops. Override to clean up resources.
42
+ def stop; end
43
+
44
+ # Seconds between ticks. Return nil to disable auto-tick.
45
+ def interval; nil; end
46
+
47
+ # Called every `interval` seconds. Override to perform periodic work.
48
+ def tick; end
49
+
50
+ # --- Shared environment data ---
51
+ #
52
+ # Services read and write the same env_data store as POs and autonomous
53
+ # ticks, scoped to the environment's active session. This replaces the old
54
+ # "__services__" sentinel thread id — heartbeat-style services no longer
55
+ # need to invent a scope, and what they store is visible to POs (and the
56
+ # live env data pane in the UI).
57
+
58
+ # Store a value in shared env_data, scoped to the active environment session.
59
+ # Creates the environment session on first write if one doesn't exist yet.
60
+ # @param key [String] Data key
61
+ # @param value [Object] JSON-serializable value
62
+ # @param short_description [String, nil] Summary for discoverability
63
+ # @return [Boolean] True if stored
64
+ def set_env_data(key, value, short_description: nil)
65
+ scope = env_data_scope(create: true)
66
+ return false unless scope
67
+
68
+ @runtime.session_store.store_env_data(
69
+ root_thread_id: scope,
70
+ key: key,
71
+ short_description: short_description || key.to_s,
72
+ value: value,
73
+ stored_by: "service:#{name}"
74
+ )
75
+ @runtime.notify_env_data_changed(
76
+ action: "set", root_thread_id: scope, key: key, stored_by: "service:#{name}"
77
+ )
78
+ true
79
+ end
80
+
81
+ # Read a value from shared env_data.
82
+ # @param key [String] Data key
83
+ # @return [Object, nil] The stored value, or nil if absent
84
+ def get_env_data(key)
85
+ scope = env_data_scope
86
+ return nil unless scope
87
+
88
+ entry = @runtime.session_store.get_env_data(root_thread_id: scope, key: key)
89
+ entry && entry[:value]
90
+ end
91
+
92
+ # List env_data keys and descriptions in the active environment session.
93
+ # @return [Array<Hash>] Entries with :key and :short_description
94
+ def list_env_data
95
+ scope = env_data_scope
96
+ return [] unless scope
97
+
98
+ @runtime.session_store.list_env_data(root_thread_id: scope)
99
+ end
100
+
101
+ # Delete an env_data entry.
102
+ # @param key [String] Data key
103
+ # @return [Boolean] True if a row was deleted
104
+ def delete_env_data(key)
105
+ scope = env_data_scope
106
+ return false unless scope
107
+
108
+ deleted = @runtime.session_store.delete_env_data(root_thread_id: scope, key: key)
109
+ if deleted
110
+ @runtime.notify_env_data_changed(
111
+ action: "delete", root_thread_id: scope, key: key, stored_by: "service:#{name}"
112
+ )
113
+ end
114
+ deleted
115
+ end
116
+
117
+ private
118
+
119
+ # Resolve the env_data scope (the active environment session). Pass
120
+ # create: true to establish the session on first write.
121
+ # @return [String, nil]
122
+ def env_data_scope(create: false)
123
+ return nil unless @runtime&.session_store
124
+
125
+ @runtime.active_session(initiated_by: "service:#{name}") if create
126
+ @runtime.env_data_scope
127
+ end
128
+ end
129
+ end