prompt_objects 0.5.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 (292) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +142 -0
  3. data/README.md +211 -19
  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 +1350 -18
  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 +190 -432
  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 +561 -52
  65. data/lib/prompt_objects/server/app.rb +134 -1
  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 -207
  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 +774 -15
  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 +78 -0
  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 +58 -0
  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 +57 -0
  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/set_env_data.rb +101 -0
  87. data/lib/prompt_objects/version.rb +5 -0
  88. data/lib/prompt_objects.rb +36 -6
  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 +6 -2
  92. data/templates/arc-agi-1/objects/solver.md +14 -4
  93. data/templates/arc-agi-1/objects/verifier.md +6 -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 +28 -1
  230. metadata +175 -65
  231. data/CLAUDE.md +0 -176
  232. data/Gemfile +0 -10
  233. data/Gemfile.lock +0 -233
  234. data/IMPLEMENTATION_PLAN.md +0 -1073
  235. data/Rakefile +0 -27
  236. data/design-doc-v2.md +0 -1232
  237. data/frontend/.gitignore +0 -3
  238. data/frontend/index.html +0 -17
  239. data/frontend/package-lock.json +0 -4540
  240. data/frontend/package.json +0 -36
  241. data/frontend/postcss.config.js +0 -6
  242. data/frontend/src/App.tsx +0 -103
  243. data/frontend/src/canvas/CanvasView.tsx +0 -113
  244. data/frontend/src/canvas/ForceLayout.ts +0 -115
  245. data/frontend/src/canvas/SceneManager.ts +0 -587
  246. data/frontend/src/canvas/canvasStore.ts +0 -47
  247. data/frontend/src/canvas/constants.ts +0 -95
  248. data/frontend/src/canvas/controls/CameraControls.ts +0 -98
  249. data/frontend/src/canvas/edges/MessageArc.ts +0 -149
  250. data/frontend/src/canvas/inspector/InspectorPanel.tsx +0 -31
  251. data/frontend/src/canvas/inspector/POInspector.tsx +0 -262
  252. data/frontend/src/canvas/inspector/ToolCallInspector.tsx +0 -67
  253. data/frontend/src/canvas/nodes/PONode.ts +0 -249
  254. data/frontend/src/canvas/nodes/ToolCallNode.ts +0 -116
  255. data/frontend/src/canvas/types.ts +0 -24
  256. data/frontend/src/components/ContextMenu.tsx +0 -68
  257. data/frontend/src/components/Inspector.tsx +0 -232
  258. data/frontend/src/components/MarkdownMessage.tsx +0 -155
  259. data/frontend/src/components/MethodList.tsx +0 -90
  260. data/frontend/src/components/ModelSelector.tsx +0 -115
  261. data/frontend/src/components/NotificationPanel.tsx +0 -130
  262. data/frontend/src/components/ObjectList.tsx +0 -78
  263. data/frontend/src/components/PaneSlot.tsx +0 -30
  264. data/frontend/src/components/SourcePane.tsx +0 -202
  265. data/frontend/src/components/SystemBar.tsx +0 -74
  266. data/frontend/src/components/Transcript.tsx +0 -76
  267. data/frontend/src/components/UsagePanel.tsx +0 -105
  268. data/frontend/src/components/Workspace.tsx +0 -260
  269. data/frontend/src/components/index.ts +0 -12
  270. data/frontend/src/hooks/useResize.ts +0 -55
  271. data/frontend/src/hooks/useWebSocket.ts +0 -517
  272. data/frontend/src/index.css +0 -103
  273. data/frontend/src/main.tsx +0 -10
  274. data/frontend/src/store/index.ts +0 -279
  275. data/frontend/src/types/index.ts +0 -165
  276. data/frontend/tailwind.config.js +0 -44
  277. data/frontend/tsconfig.json +0 -30
  278. data/frontend/vite.config.ts +0 -29
  279. data/lib/prompt_objects/server/public/assets/index-D1myxE0l.js +0 -4345
  280. data/lib/prompt_objects/server/public/assets/index-DdCcwC-Z.css +0 -1
  281. data/lib/prompt_objects/universal/add_primitive.rb +0 -118
  282. data/lib/prompt_objects/universal/ask_human.rb +0 -109
  283. data/lib/prompt_objects/universal/create_primitive.rb +0 -174
  284. data/lib/prompt_objects/universal/list_primitives.rb +0 -145
  285. data/lib/prompt_objects/universal/think.rb +0 -41
  286. data/prompt_objects.gemspec +0 -50
  287. data/templates/basic/.gitignore +0 -2
  288. data/templates/developer/.gitignore +0 -5
  289. data/templates/empty/.gitignore +0 -5
  290. data/templates/empty/objects/.gitkeep +0 -0
  291. data/templates/minimal/.gitignore +0 -5
  292. data/templates/writer/.gitignore +0 -5
@@ -0,0 +1,546 @@
1
+ # Prompt Objects Music Pro — expressive live-pattern engine for Sonic Pi.
2
+ #
3
+ # In Sonic Pi, run this one line (using the environment's absolute path):
4
+ # run_file "/ABSOLUTE/PATH/extras/sonic_pi_boot.rb"
5
+ #
6
+ # The engine receives structured OSC from Prompt Objects. Patterns are replaced
7
+ # atomically at bar boundaries; voices remain independent, synchronized live
8
+ # collaborators rather than parts in a pre-rendered song.
9
+
10
+ require "json"
11
+
12
+ use_bpm 78
13
+ set :po_bpm, 78.0
14
+ set :po_beats_per_bar, 4.0
15
+ set :po_master_amp, 0.7
16
+ set :po_max_voices, 12
17
+ set :po_voice_map, {}
18
+ set :po_next_slot, 0
19
+ set :po_patch_names, []
20
+
21
+ 12.times do |i|
22
+ set :"po_v#{i}_active", false
23
+ set :"po_v#{i}_definition", ""
24
+ set :"po_v#{i}_cycle", 0
25
+ set :"po_v#{i}_name", ""
26
+ end
27
+
28
+ define :po_clamp do |value, low, high|
29
+ [[value.to_f, low.to_f].max, high.to_f].min
30
+ end
31
+
32
+ define :po_symbolize_hash do |value|
33
+ out = {}
34
+ if value.is_a?(Hash)
35
+ value.each do |key, item|
36
+ out[key.to_s.to_sym] = item
37
+ end
38
+ end
39
+ out
40
+ end
41
+
42
+ define :po_safe_identifier do |value|
43
+ value.to_s.match?(/\A[a-zA-Z][a-zA-Z0-9_:-]{0,79}\z/)
44
+ end
45
+
46
+ define :po_store_patch do |name, patch|
47
+ return false unless po_safe_identifier(name)
48
+ return false unless patch.is_a?(Hash)
49
+ return false unless ["synth", "sample"].include?((patch["kind"] || "synth").to_s)
50
+ source = patch["synth"] || patch["sample"]
51
+ return false unless po_safe_identifier(source)
52
+ effects = patch["effects"] || []
53
+ return false unless effects.is_a?(Array) && effects.length <= 3
54
+ params = patch["params"] || {}
55
+ return false unless params.is_a?(Hash) && params.length <= 24
56
+ effects.each do |effect|
57
+ return false unless effect.is_a?(Hash) && po_safe_identifier(effect["name"])
58
+ fx_params = effect["params"] || {}
59
+ return false unless fx_params.is_a?(Hash) && fx_params.length <= 24
60
+ end
61
+
62
+ set :"po_patch_#{name}", JSON.generate(patch)
63
+ names = get(:po_patch_names) || []
64
+ unless names.include?(name.to_s)
65
+ replacement = names.to_a + [name.to_s]
66
+ set :po_patch_names, replacement
67
+ end
68
+ true
69
+ end
70
+
71
+ # These are starting points, not a whitelist. Agents can replace them or define
72
+ # completely new synth/sample/effect patches through define_patch.
73
+ po_store_patch("soft_rodeo", {
74
+ "kind" => "synth", "synth" => "rodeo",
75
+ "params" => { "amp" => 0.16, "cutoff" => 72, "release" => 1.8 },
76
+ "effects" => [{ "name" => "reverb", "params" => { "mix" => 0.18, "room" => 0.65 } }]
77
+ })
78
+ po_store_patch("air_blade", {
79
+ "kind" => "synth", "synth" => "blade",
80
+ "params" => { "amp" => 0.11, "attack" => 0.8, "release" => 3.5, "cutoff" => 72 },
81
+ "effects" => [{ "name" => "reverb", "params" => { "mix" => 0.28, "room" => 0.8 } }]
82
+ })
83
+ po_store_patch("foundation", {
84
+ "kind" => "synth", "synth" => "bass_foundation",
85
+ "params" => { "amp" => 0.15, "cutoff" => 70, "release" => 0.5 },
86
+ "effects" => []
87
+ })
88
+ po_store_patch("tiny_kalimba", {
89
+ "kind" => "synth", "synth" => "kalimba",
90
+ "params" => { "amp" => 0.1, "release" => 1.2 },
91
+ "effects" => [{ "name" => "reverb", "params" => { "mix" => 0.2, "room" => 0.7 } }]
92
+ })
93
+ po_store_patch("chip_raw", {
94
+ "kind" => "synth", "synth" => "chiplead",
95
+ "params" => { "amp" => 0.12, "release" => 0.2 },
96
+ "effects" => []
97
+ })
98
+ po_store_patch("soft_drum", {
99
+ "kind" => "sample", "sample" => "bd_haus",
100
+ "params" => { "amp" => 0.16 }, "effects" => []
101
+ })
102
+
103
+ define :po_patch do |patch_ref|
104
+ if patch_ref.is_a?(Hash)
105
+ return patch_ref
106
+ end
107
+ raw = get(:"po_patch_#{patch_ref}")
108
+ begin
109
+ raw.to_s.empty? ? JSON.parse(get(:po_patch_soft_rodeo)) : JSON.parse(raw)
110
+ rescue
111
+ JSON.parse(get(:po_patch_soft_rodeo))
112
+ end
113
+ end
114
+
115
+ define :po_voice_slot do |name|
116
+ name = name.to_s
117
+ return nil unless name.match?(/\A[a-zA-Z][a-zA-Z0-9_-]{0,39}\z/)
118
+ map = get(:po_voice_map) || {}
119
+ return map[name] unless map[name].nil?
120
+
121
+ idx = get(:po_next_slot).to_i
122
+ if idx >= get(:po_max_voices).to_i
123
+ puts "Music Pro: voice limit reached; ignored #{name}"
124
+ return nil
125
+ end
126
+ replacement = {}
127
+ map.each { |key, value| replacement[key] = value }
128
+ replacement[name] = idx
129
+ set :po_voice_map, replacement
130
+ set :po_next_slot, idx + 1
131
+ set :"po_v#{idx}_name", name
132
+ puts "Music Pro: registered #{name} in slot #{idx}"
133
+ idx
134
+ end
135
+
136
+ # Split on whitespace only at the top nesting level. This small parser keeps the
137
+ # notation concise without evaluating agent-authored Ruby code.
138
+ define :po_tokens do |text|
139
+ tokens = []
140
+ token = ""
141
+ depths = { "[" => 0, "(" => 0, "<" => 0, "{" => 0 }
142
+ closes = { "]" => "[", ")" => "(", ">" => "<", "}" => "{" }
143
+ text.to_s.each_char do |char|
144
+ if depths.key?(char)
145
+ depths[char] += 1
146
+ token += char
147
+ elsif closes.key?(char)
148
+ depths[closes[char]] -= 1
149
+ token += char
150
+ elsif char.match?(/\s/) && depths.values.all? { |depth| depth == 0 }
151
+ unless token.empty?
152
+ tokens << token
153
+ token = ""
154
+ end
155
+ else
156
+ token += char
157
+ end
158
+ end
159
+ tokens << token unless token.empty?
160
+ tokens
161
+ end
162
+
163
+ define :po_scalar do |value|
164
+ value = value.to_s.strip
165
+ return true if value == "true"
166
+ return false if value == "false"
167
+ return value.to_f if value.match?(/\A-?\d+(\.\d+)?\z/)
168
+ value
169
+ end
170
+
171
+ define :po_event_options do |raw|
172
+ options = {}
173
+ raw.to_s.split(",").each do |pair|
174
+ key, value = pair.split("=", 2)
175
+ options[key.to_s.strip] = po_scalar(value) unless key.to_s.strip.empty? || value.nil?
176
+ end
177
+ options
178
+ end
179
+
180
+ define :po_note do |value|
181
+ str = value.to_s.strip
182
+ return str.to_i if str.match?(/\A\d+\z/)
183
+ note(str.to_sym)
184
+ rescue
185
+ 60
186
+ end
187
+
188
+ define :po_parse_token do |raw_token, cycle|
189
+ token = raw_token.to_s
190
+ options = {}
191
+ if token.end_with?("}")
192
+ open = token.rindex("{")
193
+ if open
194
+ options = po_event_options(token[(open + 1)...-1])
195
+ token = token[0...open]
196
+ end
197
+ end
198
+
199
+ repeat = 1
200
+ repeat_match = token.match(/\*(\d+)\z/)
201
+ if repeat_match
202
+ repeat = po_clamp(repeat_match[1].to_i, 1, 16).to_i
203
+ token = token.sub(/\*\d+\z/, "")
204
+ end
205
+
206
+ duration = nil
207
+ duration_match = token.match(/\/([0-9]+(?:\.[0-9]+)?)\z/)
208
+ if duration_match
209
+ duration = po_clamp(duration_match[1].to_f, 0.125, 64.0)
210
+ token = token.sub(/\/[0-9]+(?:\.[0-9]+)?\z/, "")
211
+ end
212
+
213
+ events = []
214
+ if token.start_with?("[") && token.end_with?("]")
215
+ events = po_parse_pattern(token[1...-1], cycle)
216
+ if duration && !events.empty?
217
+ total = events.map { |event| event["duration"].to_f }.inject(0.0, :+)
218
+ duration_ratio = total > 0 ? duration / total : 1.0
219
+ events.each { |event| event["duration"] = event["duration"].to_f * duration_ratio }
220
+ end
221
+ elsif token.start_with?("<") && token.end_with?(">")
222
+ choices = po_tokens(token[1...-1])
223
+ chosen = choices.empty? ? "~" : choices[cycle.to_i % choices.length]
224
+ events = po_parse_token(chosen + (duration ? "/#{duration}" : ""), cycle)
225
+ else
226
+ event = { "duration" => (duration || 1.0), "options" => options }
227
+ if token == "~" || token == "-"
228
+ event["type"] = "rest"
229
+ elsif token.start_with?("@")
230
+ event["type"] = "sample"
231
+ event["sample"] = token[1..]
232
+ elsif token.start_with?("(") && token.end_with?(")")
233
+ event["type"] = "notes"
234
+ event["notes"] = token[1...-1].split(/[,+]/).map { |item| po_note(item) }.take(12)
235
+ else
236
+ event["type"] = "notes"
237
+ event["notes"] = [po_note(token)]
238
+ end
239
+ events = [event]
240
+ end
241
+
242
+ events.each do |event|
243
+ current = event["options"].is_a?(Hash) ? event["options"] : {}
244
+ event["options"] = current.merge(options)
245
+ end
246
+
247
+ repeated = []
248
+ repeat.times { events.each { |event| repeated << Marshal.load(Marshal.dump(event)) } }
249
+ repeated
250
+ end
251
+
252
+ define :po_parse_pattern do |text, cycle|
253
+ events = []
254
+ po_tokens(text.to_s[0, 4096]).take(128).each do |token|
255
+ po_parse_token(token, cycle).each do |event|
256
+ events << event if events.length < 128
257
+ end
258
+ end
259
+ events = [{ "type" => "rest", "duration" => get(:po_beats_per_bar), "options" => {} }] if events.empty?
260
+ events
261
+ rescue => error
262
+ puts "Music Pro: pattern parse error: #{error}"
263
+ [{ "type" => "rest", "duration" => get(:po_beats_per_bar), "options" => {} }]
264
+ end
265
+
266
+ define :po_transform_events do |events, transforms, cycle|
267
+ transforms = {} unless transforms.is_a?(Hash)
268
+ every = transforms["every"]
269
+ active = transforms.dup
270
+ if every.is_a?(Hash) && every["cycles"].to_i > 0 && ((cycle.to_i + 1) % every["cycles"].to_i == 0)
271
+ active[every["transform"].to_s] = every.fetch("value", true)
272
+ end
273
+ active.delete("every")
274
+
275
+ events = events.reverse if active["reverse"]
276
+ rotate = active["rotate"].to_i
277
+ events = events.rotate(rotate % events.length) unless events.empty? || rotate == 0
278
+ stretch_factor = po_clamp(active.fetch("stretch", 1.0), 0.125, 8.0)
279
+ transpose = po_clamp(active.fetch("transpose", 0), -48, 48).to_i
280
+ probability = po_clamp(active.fetch("probability", 1.0), 0.0, 1.0)
281
+ events.each do |event|
282
+ event["duration"] = po_clamp(event["duration"].to_f * stretch_factor, 0.125, 64.0)
283
+ event["notes"] = event["notes"].map { |n| n.to_f + transpose } if event["notes"]
284
+ event["options"]["probability"] = po_clamp(event["options"].fetch("probability", 1.0).to_f * probability, 0.0, 1.0)
285
+ end
286
+ events
287
+ end
288
+
289
+ define :po_play_source do |event, patch, mix|
290
+ options = event["options"] || {}
291
+ params = po_symbolize_hash(patch["params"] || {})
292
+ options.each do |key, value|
293
+ params[key.to_s.to_sym] = value unless %w[probability synth sample gate].include?(key.to_s)
294
+ end
295
+
296
+ patch_amp = params.fetch(:amp, 1.0).to_f
297
+ event_amp = options.fetch("amp", 1.0).to_f
298
+ voice_amp = mix.fetch("amp", 1.0).to_f
299
+ params[:amp] = po_clamp(patch_amp * event_amp * voice_amp * get(:po_master_amp).to_f, 0.0, 0.6)
300
+ params[:pan] = po_clamp(params.fetch(:pan, 0.0).to_f + mix.fetch("pan", 0.0).to_f, -1.0, 1.0)
301
+ params[:attack] = po_clamp(params[:attack], 0.0, 32.0) if params.key?(:attack)
302
+ params[:release] = po_clamp(params[:release], 0.0, 32.0) if params.key?(:release)
303
+ params[:sustain] = po_clamp(params[:sustain], 0.0, 64.0) if params.key?(:sustain)
304
+ params[:cutoff] = po_clamp(params[:cutoff], 20.0, 130.0) if params.key?(:cutoff)
305
+
306
+ if event["type"] == "sample" || patch["kind"].to_s == "sample"
307
+ source = options["sample"] || event["sample"] || patch["sample"]
308
+ return unless po_safe_identifier(source)
309
+ sample(source.to_s.to_sym, params)
310
+ else
311
+ source = options["synth"] || patch["synth"] || "beep"
312
+ return unless po_safe_identifier(source)
313
+ # Duration is the distance to the next onset. Unless a patch explicitly
314
+ # owns sustain, derive an envelope that remains audible for roughly that
315
+ # span. `gate` can shorten or overlap the event without changing its rhythm.
316
+ gate = po_clamp(options.fetch("gate", event["duration"].to_f * 0.95), 0.05, 64.0)
317
+ unless params.key?(:sustain)
318
+ attack = params.fetch(:attack, 0.0).to_f
319
+ unless params.key?(:release)
320
+ params[:release] = po_clamp(gate * 0.2, 0.05, 1.5)
321
+ end
322
+ params[:sustain] = po_clamp(gate - attack - params.fetch(:release, 0.0).to_f, 0.0, 64.0)
323
+ end
324
+ use_synth source.to_s.to_sym
325
+ notes = event["notes"] || [60]
326
+ notes.length > 1 ? play_chord(notes, params) : play(notes[0], params)
327
+ end
328
+ rescue => error
329
+ puts "Music Pro: event error: #{error}"
330
+ end
331
+
332
+ define :po_play_event do |event, patch, mix|
333
+ return if event["type"] == "rest"
334
+ effects = (patch["effects"] || []).take(3)
335
+ if effects.length == 0
336
+ po_play_source(event, patch, mix)
337
+ elsif effects.length == 1
338
+ with_fx effects[0]["name"].to_s.to_sym, po_symbolize_hash(effects[0]["params"] || {}) do
339
+ po_play_source(event, patch, mix)
340
+ end
341
+ elsif effects.length == 2
342
+ with_fx effects[0]["name"].to_s.to_sym, po_symbolize_hash(effects[0]["params"] || {}) do
343
+ with_fx effects[1]["name"].to_s.to_sym, po_symbolize_hash(effects[1]["params"] || {}) do
344
+ po_play_source(event, patch, mix)
345
+ end
346
+ end
347
+ else
348
+ with_fx effects[0]["name"].to_s.to_sym, po_symbolize_hash(effects[0]["params"] || {}) do
349
+ with_fx effects[1]["name"].to_s.to_sym, po_symbolize_hash(effects[1]["params"] || {}) do
350
+ with_fx effects[2]["name"].to_s.to_sym, po_symbolize_hash(effects[2]["params"] || {}) do
351
+ po_play_source(event, patch, mix)
352
+ end
353
+ end
354
+ end
355
+ end
356
+ rescue => error
357
+ puts "Music Pro: effect error: #{error}"
358
+ end
359
+
360
+ # Run one cycle inside a single FX chain. Effects are allocated per voice cycle,
361
+ # not per note, which bounds synth-node churn even for fast patterns.
362
+ define :po_run_cycle_events do |events, patch, mix, seed, cycle|
363
+ elapsed = 0.0
364
+ events.each_with_index do |event, event_index|
365
+ break if elapsed >= 128.0
366
+ probability = po_clamp(event["options"].fetch("probability", 1.0), 0.0, 1.0)
367
+ use_random_seed(seed + cycle * 521 + event_index)
368
+ po_play_source(event, patch, mix) if event["type"] != "rest" && rand < probability
369
+ duration = po_clamp(event["duration"], 0.125, 64.0)
370
+ sleep duration
371
+ elapsed += duration
372
+ end
373
+ elapsed
374
+ end
375
+
376
+ define :po_play_cycle do |events, patch, mix, seed, cycle|
377
+ effects = (patch["effects"] || []).take(3)
378
+ if effects.length == 0
379
+ po_run_cycle_events(events, patch, mix, seed, cycle)
380
+ elsif effects.length == 1
381
+ with_fx effects[0]["name"].to_s.to_sym, po_symbolize_hash(effects[0]["params"] || {}) do
382
+ po_run_cycle_events(events, patch, mix, seed, cycle)
383
+ end
384
+ elsif effects.length == 2
385
+ with_fx effects[0]["name"].to_s.to_sym, po_symbolize_hash(effects[0]["params"] || {}) do
386
+ with_fx effects[1]["name"].to_s.to_sym, po_symbolize_hash(effects[1]["params"] || {}) do
387
+ po_run_cycle_events(events, patch, mix, seed, cycle)
388
+ end
389
+ end
390
+ else
391
+ with_fx effects[0]["name"].to_s.to_sym, po_symbolize_hash(effects[0]["params"] || {}) do
392
+ with_fx effects[1]["name"].to_s.to_sym, po_symbolize_hash(effects[1]["params"] || {}) do
393
+ with_fx effects[2]["name"].to_s.to_sym, po_symbolize_hash(effects[2]["params"] || {}) do
394
+ po_run_cycle_events(events, patch, mix, seed, cycle)
395
+ end
396
+ end
397
+ end
398
+ end
399
+ rescue => error
400
+ puts "Music Pro: cycle effect error: #{error}"
401
+ 0.0
402
+ end
403
+
404
+ live_loop :po_music_clock do
405
+ use_bpm get(:po_bpm)
406
+ cue :po_music_bar
407
+ sleep get(:po_beats_per_bar)
408
+ end
409
+
410
+ with_fx :compressor, threshold: 0.2, slope_above: 0.35, clamp_time: 0.01, relax_time: 0.08 do
411
+ 12.times do |i|
412
+ live_loop :"po_music_voice_#{i}" do
413
+ sync :po_music_bar
414
+ use_bpm get(:po_bpm)
415
+ next unless get(:"po_v#{i}_active")
416
+ raw = get(:"po_v#{i}_definition").to_s
417
+ next if raw.empty?
418
+ begin
419
+ definition = JSON.parse(raw)
420
+ cycle = get(:"po_v#{i}_cycle").to_i
421
+ events = po_parse_pattern(definition["pattern"], cycle)
422
+ events = po_transform_events(events, definition["transforms"], cycle)
423
+ patch = po_patch(definition["patch"])
424
+ mix = definition["mix"].is_a?(Hash) ? definition["mix"] : {}
425
+ seed = (definition.dig("transforms", "seed") || 1000).to_i
426
+ elapsed = po_play_cycle(events, patch, mix, seed, cycle)
427
+ bar = get(:po_beats_per_bar).to_f
428
+ remainder = (bar - (elapsed % bar)) % bar
429
+ sleep remainder if remainder > 0.001
430
+ set :"po_v#{i}_cycle", cycle + 1
431
+ rescue => error
432
+ puts "Music Pro: voice #{i} error: #{error}"
433
+ end
434
+ end
435
+ end
436
+ end
437
+
438
+ live_loop :po_osc_pattern do
439
+ args = sync "/osc*/prompt_objects/music/pattern"
440
+ begin
441
+ raw = args[0].to_s
442
+ raise "definition too large" if raw.bytesize > 16_384
443
+ definition = JSON.parse(raw)
444
+ idx = po_voice_slot(definition["voice"])
445
+ next if idx.nil?
446
+ po_parse_pattern(definition["pattern"].to_s, 0) # validate before replacing live state
447
+ set :"po_v#{idx}_definition", JSON.generate(definition)
448
+ set :"po_v#{idx}_active", true
449
+ puts "Music Pro: queued #{definition['voice']} on next bar"
450
+ rescue => error
451
+ puts "Music Pro: rejected pattern: #{error}"
452
+ end
453
+ end
454
+
455
+ live_loop :po_osc_patch do
456
+ args = sync "/osc*/prompt_objects/music/patch"
457
+ begin
458
+ patch = JSON.parse(args[1].to_s)
459
+ result = po_store_patch(args[0].to_s, patch)
460
+ puts(result ? "Music Pro: defined patch #{args[0]}" : "Music Pro: rejected patch #{args[0]}")
461
+ rescue => error
462
+ puts "Music Pro: rejected patch: #{error}"
463
+ end
464
+ end
465
+
466
+ live_loop :po_osc_tempo do
467
+ args = sync "/osc*/prompt_objects/music/tempo"
468
+ bpm = args[0].to_f
469
+ if bpm >= 30 && bpm <= 240
470
+ set :po_bpm, bpm
471
+ puts "Music Pro: tempo #{bpm} BPM"
472
+ end
473
+ end
474
+
475
+ live_loop :po_osc_stop do
476
+ args = sync "/osc*/prompt_objects/music/stop"
477
+ name = args[0].to_s
478
+ if name == "all"
479
+ 12.times { |i| set :"po_v#{i}_active", false }
480
+ puts "Music Pro: all voices stopped"
481
+ else
482
+ map = get(:po_voice_map) || {}
483
+ idx = map[name]
484
+ set :"po_v#{idx}_active", false unless idx.nil?
485
+ end
486
+ end
487
+
488
+ live_loop :po_osc_trigger do
489
+ args = sync "/osc*/prompt_objects/music/trigger"
490
+ begin
491
+ definition = JSON.parse(args[0].to_s)
492
+ events = po_parse_pattern(definition["pattern"], 0).take(1)
493
+ patch = po_patch(definition["patch"])
494
+ in_thread do
495
+ with_fx :compressor, threshold: 0.2, slope_above: 0.35, clamp_time: 0.01, relax_time: 0.08 do
496
+ po_play_event(events[0], patch, {}) unless events.empty?
497
+ end
498
+ end
499
+ rescue => error
500
+ puts "Music Pro: trigger error: #{error}"
501
+ end
502
+ end
503
+
504
+ live_loop :po_osc_query do
505
+ args = sync "/osc*/prompt_objects/music/query"
506
+ response_port = args[0].to_i
507
+ next if response_port <= 0
508
+ map = get(:po_voice_map) || {}
509
+ voices = {}
510
+ map.each do |name, idx|
511
+ raw = get(:"po_v#{idx}_definition").to_s
512
+ definition = raw.empty? ? {} : JSON.parse(raw)
513
+ patch_ref = definition["patch"]
514
+ patch_state = if patch_ref.is_a?(Hash)
515
+ {
516
+ "inline" => true,
517
+ "kind" => patch_ref["kind"],
518
+ "source" => patch_ref["synth"] || patch_ref["sample"],
519
+ "effects" => (patch_ref["effects"] || []).map { |effect| effect["name"] }
520
+ }
521
+ else
522
+ patch_ref
523
+ end
524
+ voices[name] = {
525
+ "active" => !!get(:"po_v#{idx}_active"),
526
+ "cycle" => get(:"po_v#{idx}_cycle").to_i,
527
+ "pattern" => definition["pattern"],
528
+ "patch" => patch_state,
529
+ "transforms" => definition["transforms"],
530
+ "mix" => definition["mix"]
531
+ }
532
+ end
533
+ state = {
534
+ "connected" => true,
535
+ "engine" => "music-pro/1",
536
+ "bpm" => get(:po_bpm).to_f,
537
+ "beats_per_bar" => get(:po_beats_per_bar).to_f,
538
+ "voice_limit" => get(:po_max_voices).to_i,
539
+ "master_amp" => get(:po_master_amp).to_f,
540
+ "patches" => get(:po_patch_names),
541
+ "voices" => voices
542
+ }
543
+ osc_send "127.0.0.1", response_port, "/prompt_objects/music/state", JSON.generate(state)
544
+ end
545
+
546
+ puts "Prompt Objects Music Pro loaded — 12 live voices, expressive patterns, OSC on 4560."
@@ -0,0 +1,19 @@
1
+ name: music-pro
2
+ description: Expressive multi-agent live composition through a Strudel-inspired Sonic Pi pattern surface
3
+ icon: "🎛️"
4
+ color: "#8B5CF6"
5
+
6
+ dependencies:
7
+ gems:
8
+ - name: osc-ruby
9
+ version: "~> 1.1"
10
+ purpose: "OSC communication with Sonic Pi"
11
+
12
+ objects:
13
+ - conductor
14
+ - sonic_pi
15
+ - voice_harmony
16
+ - voice_rhythm
17
+ - voice_texture
18
+
19
+ default_po: conductor
@@ -0,0 +1,126 @@
1
+ ---
2
+ name: conductor
3
+ description: Live ensemble director — establishes a musical contract and directs independent voice POs as they rewrite a shared, evolving composition
4
+ delegates_to:
5
+ - sonic_pi
6
+ - voice_harmony
7
+ - voice_rhythm
8
+ - voice_texture
9
+ tools:
10
+ - query_music_engine
11
+ - set_tempo
12
+ - stop_voice
13
+ autonomous:
14
+ interval: 24
15
+ wait_for_human: true
16
+ message: |
17
+ Evolve the running composition only if Sonic Pi is connected and music is active.
18
+ Query the engine and read the composition and voice state, then develop the
19
+ next recognizable movement of the same piece. Choose one musically motivated
20
+ intention and direct TWO or THREE compatible voices when an ensemble response
21
+ serves it; a genuinely sectional single-voice change is also valid. Preserve
22
+ tempo, harmonic identity, a recognizable motif or relationship, and at least
23
+ one unchanged active voice as an anchor. Do not choose a canned scene, dictate
24
+ literal replacement patterns, or rewrite the whole ensemble.
25
+
26
+ Require every affected musician to perform `set_live_pattern` in the same
27
+ turn. Dispatch independent directions in parallel, join every run, then query
28
+ the engine. A prose proposal is not a change. Record the movement in env_data
29
+ only when live state confirms the rewrites. Maintain a legible connective
30
+ layer; space should shape the music, not make the whole ensemble feel stalled.
31
+ ---
32
+
33
+ # Conductor
34
+
35
+ You direct a live ensemble of Prompt Objects. The demonstration is the collaboration itself: autonomous musical identities communicate, negotiate roles, and continually rewrite patterns that Sonic Pi is playing. You are not a playlist, scene selector, or step sequencer.
36
+
37
+ ## Execution and verification contract
38
+
39
+ A request to start or evolve the composition is authorization to perform it.
40
+ Do not ask musicians what they merely *propose*, and do not wait for additional
41
+ human confirmation. Direct each musician to choose its final material and call
42
+ `set_live_pattern` in that same turn. A musician defining a new patch must call
43
+ `define_patch` first and then activate it with `set_live_pattern`.
44
+
45
+ A musician's prose, notation block, or claim that a part is playing is not
46
+ evidence. After every delegated activation or revision, call
47
+ `query_music_engine`. Do not tell the human a voice is sounding and do not move
48
+ to the next musician unless live state contains that voice, marks it active,
49
+ and shows the expected pattern and patch. If a musician only describes a
50
+ pattern or asks permission, redirect it explicitly: perform the playback tool
51
+ calls now, do not propose, and report the tool results.
52
+
53
+ Calling a voice directly waits for its response; use that when the next musical
54
+ decision depends on the result. Use `dispatch_po` for independent voice changes
55
+ that can proceed concurrently, retain every returned `run_id`, and call
56
+ `join_runs` before querying the engine or reporting the combined change. For a
57
+ fire-and-follow-up change, provide `on_complete` with the exact verification you
58
+ want performed when that voice finishes. Never treat dispatch as evidence that
59
+ the sound changed—the same live-state verification rules still apply.
60
+
61
+ ## Begin with a contract
62
+
63
+ Check the connection through `sonic_pi`, set one shared tempo, then write `composition` to env_data. Include genre or aesthetic, key/scale, meter, harmonic rhythm, register boundaries, target density, and a short motif or constraint the ensemble can recognize. Adapt it to the human's request rather than always using the same progression.
64
+
65
+ For a first audition, a safe but non-mandatory contract is D dorian, 92 BPM,
66
+ 4/4, one bar per chord, progression Dm9–G6/9, flowing-light density. Name a
67
+ connective layer—sustained harmony, a recurring motif, or a quiet pulse—and set
68
+ a maximum ensemble-wide gap of two beats. Safety and taste are separate: the
69
+ engine prevents damaging output levels, while you and the voices make the
70
+ aesthetic decisions.
71
+
72
+ ## Direct musicians, not parameters
73
+
74
+ You own `set_tempo`, `query_music_engine`, and the emergency `stop_voice`. Voice POs own `set_live_pattern`, `define_patch`, and `trigger_event`. Give them semantic direction:
75
+
76
+ - “Harmony, keep the two-chord identity but invert the upper voices and create more air.”
77
+ - “Rhythm, answer the texture's three-beat gesture without increasing overall density.”
78
+ - “Texture, derive a tiny counter-motif from the harmony and let it appear every fourth cycle.”
79
+
80
+ Ask voices to negotiate if their registers or rhythms collide. Never silently write a voice's pattern yourself. A live query shows structure and activity, not sound quality; treat the human's listening feedback as authoritative.
81
+
82
+ For initial playback, direct and verify harmony first, then texture, then rhythm
83
+ unless harmony or texture already provides an unmistakable pulse. The initial
84
+ ensemble should sound like a phrase in motion, not isolated demonstrations of
85
+ three sounds. Use imperative language such as “compose and
86
+ activate your part now; call `set_live_pattern` before responding,” never “what
87
+ do you propose?” Do not proceed to the next musician while the queried engine
88
+ still omits the requested voice.
89
+
90
+ ## Continuity without constraint
91
+
92
+ The pro surface remains open-ended, but openness is not an excuse for accidental
93
+ gaps. At least one active voice must provide continuity at all times: a sustained
94
+ field, a connected recurring phrase, or a quiet legible pulse. Other voices may
95
+ be sparse around that anchor. Durations set the next onset and synth gates follow
96
+ them by default; ask for explicit `gate` articulation when a voice should detach
97
+ or overlap deliberately. Do not let every musician independently choose long
98
+ rests, slow attacks, and short releases in the same section.
99
+
100
+ For speech-safe music, create space through low level, register separation,
101
+ simple voicing, and limited simultaneous activity—not by stretching every event
102
+ far apart. Preserve a phrase for several cycles before transforming it. A new
103
+ movement should change one or two dimensions such as orchestration, density,
104
+ register, articulation, or rhythmic energy while retaining an audible anchor.
105
+
106
+ ## Evolution
107
+
108
+ Preserve recognizable material. On a normal autonomous tick, choose one
109
+ section-level intention and coordinate two or three voices when it requires an
110
+ ensemble response. Favor variation techniques—rotation, thinning, register
111
+ shift, inversion, altered articulation, call-and-response, foreground/background
112
+ exchange—over replacing everything. Keep at least one active voice unchanged as
113
+ a continuity anchor. Large harmonic or tempo changes belong at infrequent
114
+ section boundaries and must be communicated to every affected voice.
115
+
116
+ The starting musicians are harmony, rhythm, and texture. You may create more Prompt Object voices when the music needs a genuinely distinct role. Give each new musician these capabilities:
117
+
118
+ `["set_live_pattern", "define_patch", "trigger_event", "stop_voice"]`
119
+
120
+ Its identity must specify a role and listening relationship, but should not hard-code a sound or pattern. The engine supports at most twelve simultaneous voice names.
121
+
122
+ ## Human feedback and performance safety
123
+
124
+ Ask for concrete feedback: too bright, muddy, repetitive, busy, static, loud, or distracting. Translate that into musical direction. Keep `stop_voice(voice: "all")` available as an emergency mute. Start with low voice amps (roughly 0.2–0.6 relative to already-quiet patches), add one voice at a time, and query after changes.
125
+
126
+ The engine clamps final event amplitude, tempo, effects per patch, voice count, pattern size, event count, and cycle duration, and routes the ensemble through a master compressor. Those are guardrails, not a style system: raw synths, built-in samples, effects, parameters, runtime patches, chords, and transformations remain available.