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,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptObjects
4
+ module Execution
5
+ # Bounded worker pool for synchronous adapters and capabilities. Completion
6
+ # is signalled through an IO so an Async fiber yields instead of blocking
7
+ # the server reactor while a worker is occupied.
8
+ class BlockingExecutor
9
+ class RejectedError < StandardError; end
10
+
11
+ Job = Data.define(:work, :completion, :result)
12
+ STOP = Object.new.freeze
13
+
14
+ def initialize(size: 4, queue_capacity: size * 4)
15
+ raise ArgumentError, "size must be positive" unless size.positive?
16
+ raise ArgumentError, "queue_capacity must be positive" unless queue_capacity.positive?
17
+
18
+ @jobs = SizedQueue.new(queue_capacity)
19
+ @state_mutex = Mutex.new
20
+ @closed = false
21
+ @workers = Array.new(size) { start_worker }
22
+ end
23
+
24
+ def call(&work)
25
+ raise ArgumentError, "a block is required" unless work
26
+
27
+ reader, writer = IO.pipe
28
+ result = {}
29
+ enqueue(Job.new(work: work, completion: writer, result: result))
30
+ reader.read(1)
31
+ raise result[:error] if result.key?(:error)
32
+
33
+ result[:value]
34
+ ensure
35
+ reader&.close unless reader&.closed?
36
+ writer&.close unless writer&.closed?
37
+ end
38
+
39
+ def close
40
+ workers = @state_mutex.synchronize do
41
+ return if @closed
42
+
43
+ @closed = true
44
+ @workers.dup
45
+ end
46
+ workers.size.times { @jobs.push(STOP) }
47
+ workers.each(&:join)
48
+ end
49
+
50
+ private
51
+
52
+ def enqueue(job)
53
+ @state_mutex.synchronize do
54
+ raise RejectedError, "blocking executor is closed" if @closed
55
+
56
+ @jobs.push(job, true)
57
+ end
58
+ rescue ThreadError
59
+ raise RejectedError, "blocking executor queue is full"
60
+ end
61
+
62
+ def start_worker
63
+ Thread.new do
64
+ Thread.current.name = "prompt-objects-worker" if Thread.current.respond_to?(:name=)
65
+ while (job = @jobs.pop)
66
+ break if job.equal?(STOP)
67
+
68
+ execute(job)
69
+ end
70
+ end
71
+ end
72
+
73
+ def execute(job)
74
+ job.result[:value] = job.work.call
75
+ rescue Exception => e # rubocop:disable Lint/RescueException -- propagate worker failures to the caller
76
+ job.result[:error] = e
77
+ ensure
78
+ begin
79
+ job.completion.write(".")
80
+ rescue IOError, Errno::EPIPE
81
+ # The awaiting Async task was cancelled; the work is allowed to finish.
82
+ ensure
83
+ job.completion.close unless job.completion.closed?
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptObjects
4
+ module Execution
5
+ class CancelledError < StandardError; end
6
+
7
+ # Cooperative cancellation shared by a run and its attached descendants.
8
+ class CancellationToken
9
+ def initialize
10
+ @cancelled = false
11
+ @reason = nil
12
+ @mutex = Mutex.new
13
+ end
14
+
15
+ def cancel(reason = "cancelled")
16
+ @mutex.synchronize do
17
+ @cancelled = true
18
+ @reason = reason
19
+ end
20
+ end
21
+
22
+ def cancelled?
23
+ @mutex.synchronize { @cancelled }
24
+ end
25
+
26
+ def reason
27
+ @mutex.synchronize { @reason }
28
+ end
29
+
30
+ def check!
31
+ raise CancelledError, reason if cancelled?
32
+ end
33
+ end
34
+
35
+ # Immutable identity and dependencies passed to one run or tool call.
36
+ class Context
37
+ attr_reader :env, :bus, :human_queue, :workspace_session_id, :thread_id,
38
+ :run_id, :caller_po, :target_po, :source_type, :source_name,
39
+ :current_capability, :calling_po, :cancellation_token,
40
+ :trace_id, :span_id, :deadline, :resource_lock_manager,
41
+ :event_publisher, :tui_mode
42
+
43
+ def initialize(env:, workspace_session_id:, thread_id:, run_id:, target_po:,
44
+ source_type:, source_name: nil, caller_po: nil,
45
+ current_capability: nil, cancellation_token: CancellationToken.new,
46
+ trace_id: SecureRandom.uuid, span_id: SecureRandom.uuid,
47
+ deadline: nil, resource_lock_manager: nil, event_publisher: nil,
48
+ tui_mode: false)
49
+ @env = env
50
+ @bus = env.bus
51
+ @human_queue = env.human_queue
52
+ @workspace_session_id = workspace_session_id
53
+ @thread_id = thread_id
54
+ @run_id = run_id
55
+ @caller_po = caller_po
56
+ @target_po = target_po
57
+ @source_type = source_type
58
+ @source_name = source_name
59
+ @current_capability = current_capability
60
+ @calling_po = caller_po
61
+ @cancellation_token = cancellation_token
62
+ @trace_id = trace_id
63
+ @span_id = span_id
64
+ @deadline = deadline
65
+ @resource_lock_manager = resource_lock_manager
66
+ @event_publisher = event_publisher
67
+ @tui_mode = tui_mode
68
+ freeze
69
+ end
70
+
71
+ def for_tool_call(tool_call)
72
+ self.class.new(
73
+ **shared_attributes,
74
+ caller_po: target_po,
75
+ current_capability: tool_call.name,
76
+ span_id: SecureRandom.uuid
77
+ )
78
+ end
79
+
80
+ def for_child_run(run_id:, thread_id:, target_po:, source_name: nil)
81
+ self.class.new(
82
+ **shared_attributes,
83
+ run_id: run_id,
84
+ thread_id: thread_id,
85
+ target_po: target_po,
86
+ caller_po: self.target_po,
87
+ source_type: "delegation",
88
+ source_name: source_name || self.target_po,
89
+ current_capability: nil,
90
+ span_id: SecureRandom.uuid
91
+ )
92
+ end
93
+
94
+ def env_data_scope
95
+ workspace_session_id
96
+ end
97
+
98
+ def check_cancelled!
99
+ cancellation_token.check!
100
+ return true unless deadline && Time.now >= deadline
101
+
102
+ raise CancelledError, "deadline exceeded"
103
+ end
104
+
105
+ def log_message(to:, message:)
106
+ bus.publish(from: current_capability || source_name || source_type, to: to, message: message)
107
+ end
108
+
109
+ def log_response(from:, message:)
110
+ bus.publish(from: from, to: current_capability || target_po, message: message)
111
+ end
112
+
113
+ private
114
+
115
+ def shared_attributes
116
+ {
117
+ env: env,
118
+ workspace_session_id: workspace_session_id,
119
+ thread_id: thread_id,
120
+ run_id: run_id,
121
+ target_po: target_po,
122
+ source_type: source_type,
123
+ source_name: source_name,
124
+ caller_po: caller_po,
125
+ current_capability: current_capability,
126
+ cancellation_token: cancellation_token,
127
+ trace_id: trace_id,
128
+ span_id: span_id,
129
+ deadline: deadline,
130
+ resource_lock_manager: resource_lock_manager,
131
+ event_publisher: event_publisher,
132
+ tui_mode: tui_mode
133
+ }
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptObjects
4
+ module Execution
5
+ # Stable names shared by the runtime protocol, persistence layer, and UI.
6
+ # The protocol fixtures in protocol/v1/fixtures are the wire-format examples.
7
+ module Contracts
8
+ PROTOCOL_VERSION = 1
9
+
10
+ THREAD_TYPES = %w[human autonomous artifact delegation service api].freeze
11
+ RUN_STATUSES = %w[queued running waiting completed failed cancelled skipped interrupted].freeze
12
+ TOOL_EXECUTION_STATUSES = %w[pending running completed failed cancelled timed_out].freeze
13
+ RUN_EVENT_KINDS = %w[
14
+ run_queued run_started model_started stream_delta model_completed
15
+ tool_batch_started tool_started tool_completed tool_failed tool_cancelled
16
+ delegation_started delegation_completed artifact_upserted env_data_changed
17
+ run_waiting run_completed run_failed run_cancelled run_skipped run_interrupted
18
+ ].freeze
19
+
20
+ SNAPSHOT_TYPES = %w[workspace_snapshot thread_snapshot artifact_snapshot].freeze
21
+ MUTATION_TYPES = %w[artifact_upserted artifact_removed env_data_changed].freeze
22
+ ENVELOPE_TYPES = (SNAPSHOT_TYPES + MUTATION_TYPES + ["run_event"]).freeze
23
+
24
+ module_function
25
+
26
+ def validate_envelope!(envelope)
27
+ data = stringify_keys(envelope)
28
+ version = data.fetch("version")
29
+ type = data.fetch("type")
30
+ payload = data.fetch("payload")
31
+
32
+ raise ArgumentError, "unsupported protocol version: #{version}" unless version == PROTOCOL_VERSION
33
+ raise ArgumentError, "unknown protocol message type: #{type}" unless ENVELOPE_TYPES.include?(type)
34
+ raise ArgumentError, "payload must be an object" unless payload.is_a?(Hash)
35
+
36
+ validate_run_event!(payload) if type == "run_event"
37
+ true
38
+ end
39
+
40
+ def validate_run_event!(payload)
41
+ %w[workspace_session_id thread_id run_id sequence workspace_cursor kind timestamp data].each do |key|
42
+ payload.fetch(key)
43
+ end
44
+
45
+ kind = payload.fetch("kind")
46
+ raise ArgumentError, "unknown run event kind: #{kind}" unless RUN_EVENT_KINDS.include?(kind)
47
+ raise ArgumentError, "run event sequence must be positive" unless payload.fetch("sequence").to_i.positive?
48
+ raise ArgumentError, "workspace cursor must be positive" unless payload.fetch("workspace_cursor").to_i.positive?
49
+ end
50
+
51
+ def stringify_keys(value)
52
+ return value.transform_keys(&:to_s).transform_values { |item| stringify_keys(item) } if value.is_a?(Hash)
53
+ return value.map { |item| stringify_keys(item) } if value.is_a?(Array)
54
+
55
+ value
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptObjects
4
+ module Execution
5
+ # A small counting semaphore used for scheduler resources that must remain
6
+ # distinct (workers, providers, and later resource leases).
7
+ class PermitPool
8
+ def initialize(limit)
9
+ raise ArgumentError, "limit must be positive" unless limit.positive?
10
+
11
+ @limit = limit
12
+ @available = limit
13
+ @mutex = Mutex.new
14
+ @condition = ConditionVariable.new
15
+ end
16
+
17
+ attr_reader :limit
18
+
19
+ def with
20
+ acquire
21
+ yield
22
+ ensure
23
+ release
24
+ end
25
+
26
+ def acquire
27
+ @mutex.synchronize do
28
+ @condition.wait(@mutex) until @available.positive?
29
+ @available -= 1
30
+ end
31
+ true
32
+ end
33
+
34
+ def release
35
+ @mutex.synchronize do
36
+ raise ThreadError, "permit pool released without an acquisition" if @available >= @limit
37
+
38
+ @available += 1
39
+ @condition.signal
40
+ end
41
+ true
42
+ end
43
+
44
+ def available
45
+ @mutex.synchronize { @available }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptObjects
4
+ module Execution
5
+ ResolvedPolicy = Data.define(:parallel_safe, :side_effect, :workload, :resource_keys, :signature)
6
+
7
+ class Policy
8
+ SIDE_EFFECTS = %i[read write exclusive].freeze
9
+ WORKLOADS = %i[blocking cpu].freeze
10
+
11
+ def initialize(parallel_safe:, side_effect:, resource_keys: nil, workload: :blocking)
12
+ raise ArgumentError, "unknown side effect: #{side_effect}" unless SIDE_EFFECTS.include?(side_effect.to_sym)
13
+ raise ArgumentError, "unknown workload: #{workload}" unless WORKLOADS.include?(workload.to_sym)
14
+
15
+ @parallel_safe = !!parallel_safe
16
+ @side_effect = side_effect.to_sym
17
+ @workload = workload.to_sym
18
+ @resource_resolver = resource_keys
19
+ freeze
20
+ end
21
+
22
+ attr_reader :parallel_safe, :side_effect, :workload
23
+
24
+ def resolve(arguments:, context:, capability:)
25
+ keys = if @resource_resolver
26
+ Array(@resource_resolver.call(arguments, context, capability))
27
+ elsif side_effect == :exclusive
28
+ ["workspace:#{context.workspace_session_id}:exclusive"]
29
+ else
30
+ []
31
+ end
32
+ ResolvedPolicy.new(
33
+ parallel_safe: parallel_safe,
34
+ side_effect: side_effect,
35
+ workload: workload,
36
+ resource_keys: keys.compact.map(&:to_s).uniq.sort.freeze,
37
+ signature: signature(capability).freeze
38
+ )
39
+ end
40
+
41
+ def signature(capability = nil)
42
+ resolver_location = @resource_resolver&.source_location&.join(":")
43
+ [capability&.class&.name, parallel_safe, side_effect, workload, resolver_location].compact.join("|")
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "timeout"
5
+
6
+ module PromptObjects
7
+ module Execution
8
+ # Atomically leases a set of resource keys. All-or-nothing acquisition
9
+ # avoids deadlocks when two tool calls need overlapping key sets.
10
+ class ResourceLockManager
11
+ class LockTimeoutError < Timeout::Error; end
12
+
13
+ def initialize
14
+ @leased = Set.new
15
+ @mutex = Mutex.new
16
+ @condition = ConditionVariable.new
17
+ end
18
+
19
+ def with(resource_keys, timeout: 30, cancellation_token: nil)
20
+ keys = Array(resource_keys).compact.map(&:to_s).uniq.sort
21
+ return yield if keys.empty?
22
+
23
+ acquire(keys, timeout: timeout, cancellation_token: cancellation_token)
24
+ acquired = true
25
+ yield
26
+ ensure
27
+ release(keys) if acquired
28
+ end
29
+
30
+ def locked_keys
31
+ @mutex.synchronize { @leased.to_a.sort }
32
+ end
33
+
34
+ private
35
+
36
+ def acquire(keys, timeout:, cancellation_token:)
37
+ deadline = monotonic_time + timeout
38
+ @mutex.synchronize do
39
+ until (@leased & keys).empty?
40
+ cancellation_token&.check!
41
+ remaining = deadline - monotonic_time
42
+ raise LockTimeoutError, "timed out waiting for resources: #{keys.join(', ')}" unless remaining.positive?
43
+
44
+ @condition.wait(@mutex, [remaining, 0.05].min)
45
+ end
46
+ cancellation_token&.check!
47
+ @leased.merge(keys)
48
+ end
49
+ end
50
+
51
+ def release(keys)
52
+ @mutex.synchronize do
53
+ keys.each { |key| @leased.delete(key) }
54
+ @condition.broadcast
55
+ end
56
+ end
57
+
58
+ def monotonic_time
59
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
60
+ end
61
+ end
62
+ end
63
+ end