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,498 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PromptObjects
4
+ module Execution
5
+ # Executes one persisted run to completion without owning scheduler policy.
6
+ # The scheduler may call this concurrently for different leased threads.
7
+ class Runner
8
+ class CapabilityError < StandardError; end
9
+ PreparedTool = Data.define(:tool_call, :execution, :policy, :policy_error)
10
+
11
+ def initialize(runtime, event_publisher: nil, llm_resolver: nil, provider_permits: nil,
12
+ tool_permits: nil, cpu_permits: nil, resource_lock_manager: nil)
13
+ @runtime = runtime
14
+ @event_publisher = event_publisher
15
+ @llm_resolver = llm_resolver || ->(_definition) { runtime.llm }
16
+ @provider_permits = provider_permits
17
+ @tool_permits = tool_permits
18
+ @cpu_permits = cpu_permits
19
+ @resource_lock_manager = resource_lock_manager
20
+ store = runtime.session_store
21
+ raise ArgumentError, "run execution requires a session store" unless store
22
+
23
+ @threads = Repositories::ThreadRepository.new(store)
24
+ @runs = Repositories::RunRepository.new(store)
25
+ @definitions = Repositories::DefinitionRepository.new(store)
26
+ @messages = Repositories::MessageRepository.new(store)
27
+ @events = Repositories::EventRepository.new(store)
28
+ @tools = Repositories::ToolExecutionRepository.new(store)
29
+ end
30
+
31
+ def execute(run_id, cancellation_token: CancellationToken.new, tui_mode: false)
32
+ run = required_run(run_id)
33
+ definition = required_definition(run_id)
34
+ prompt_object = required_prompt_object(run.po_name)
35
+ context = build_context(run, cancellation_token, tui_mode)
36
+
37
+ start_run(run, context)
38
+ history = history_for(run)
39
+ loop do
40
+ context.check_cancelled!
41
+ response = call_model(prompt_object, definition, history, run)
42
+ context.check_cancelled!
43
+ if response.tool_calls?
44
+ persist_assistant_tool_message(run, response)
45
+ results = execute_tool_batch(run, definition, response.tool_calls, context)
46
+ persist_tool_results(run, results)
47
+ history = history_for(run)
48
+ else
49
+ return complete_run(run, response)
50
+ end
51
+ end
52
+ rescue CancelledError => e
53
+ cancel_run(run_id, e.message)
54
+ raise
55
+ rescue => e
56
+ fail_run(run_id, e)
57
+ raise
58
+ end
59
+
60
+ private
61
+
62
+ def required_run(run_id)
63
+ run = @runs.get(run_id)
64
+ raise ArgumentError, "unknown run: #{run_id}" unless run
65
+ raise ArgumentError, "run #{run_id} is #{run.status}, expected queued" unless run.status == "queued"
66
+
67
+ run
68
+ end
69
+
70
+ def required_definition(run_id)
71
+ definition = @definitions.get(run_id)
72
+ raise ArgumentError, "run #{run_id} has no definition snapshot" unless definition
73
+
74
+ definition
75
+ end
76
+
77
+ def required_prompt_object(name)
78
+ prompt_object = @runtime.registry.get(name)
79
+ raise ArgumentError, "unknown prompt object: #{name}" unless prompt_object.is_a?(PromptObject)
80
+
81
+ prompt_object
82
+ end
83
+
84
+ def build_context(run, cancellation_token, tui_mode)
85
+ Context.new(
86
+ env: @runtime,
87
+ workspace_session_id: run.workspace_session_id,
88
+ thread_id: run.thread_id,
89
+ run_id: run.id,
90
+ target_po: run.po_name,
91
+ caller_po: caller_for(run),
92
+ source_type: run.source_type,
93
+ source_name: run.source_name,
94
+ cancellation_token: cancellation_token,
95
+ deadline: run_deadline(run),
96
+ resource_lock_manager: @resource_lock_manager,
97
+ event_publisher: @event_publisher,
98
+ tui_mode: tui_mode
99
+ )
100
+ end
101
+
102
+ def caller_for(run)
103
+ return nil unless run.parent_run_id
104
+
105
+ @runs.get(run.parent_run_id)&.po_name
106
+ end
107
+
108
+ def start_run(run, context)
109
+ context.check_cancelled!
110
+ running = @runs.transition(run.id, status: "running")
111
+ publish_event(@events.append_run_event(
112
+ run_id: run.id,
113
+ kind: "run_started",
114
+ data: { source_type: run.source_type, turn_ordinal: run.turn_ordinal }
115
+ ))
116
+ input = @messages.append(
117
+ thread_id: run.thread_id,
118
+ run_id: run.id,
119
+ turn_ordinal: run.turn_ordinal,
120
+ role: :user,
121
+ content: run.input_content,
122
+ from_po: run.source_name || run.source_type
123
+ )
124
+ @runs.transition(
125
+ run.id,
126
+ status: running.status,
127
+ input_message_id: input[:id],
128
+ history_cutoff_message_id: input[:id]
129
+ )
130
+ # The input is durable now, so expose it immediately. Waiting for the
131
+ # first tool result or final model response makes a freshly submitted
132
+ # message appear to vanish while the run is thinking.
133
+ @runtime.publish_thread_snapshot(run.thread_id, po_name: run.po_name)
134
+ end
135
+
136
+ def call_model(prompt_object, definition, history, run)
137
+ publish_event(@events.append_run_event(run_id: run.id, kind: "model_started"))
138
+ request = lambda do
139
+ @llm_resolver.call(definition).chat(
140
+ system: prompt_object.system_prompt_for(definition),
141
+ messages: history,
142
+ tools: prompt_object.tool_descriptors_for(definition)
143
+ )
144
+ end
145
+ response = @provider_permits ? @provider_permits.with(&request) : request.call
146
+ publish_event(@events.append_run_event(
147
+ run_id: run.id,
148
+ kind: "model_completed",
149
+ data: { tool_call_count: response.tool_calls.size, usage: response.usage }
150
+ ))
151
+ response
152
+ end
153
+
154
+ def persist_assistant_tool_message(run, response)
155
+ @messages.append(
156
+ thread_id: run.thread_id,
157
+ run_id: run.id,
158
+ turn_ordinal: run.turn_ordinal,
159
+ role: :assistant,
160
+ content: response.content,
161
+ tool_calls: response.tool_calls.map(&:to_h),
162
+ usage: response.usage
163
+ )
164
+ @runtime.publish_thread_snapshot(run.thread_id, po_name: run.po_name)
165
+ end
166
+
167
+ def execute_tool_batch(run, definition, tool_calls, context)
168
+ batch_id = SecureRandom.uuid
169
+ prepared = tool_calls.each_with_index.map do |tool_call, position|
170
+ capability = @runtime.registry.get(tool_call.name)
171
+ policy, policy_error = resolve_policy(capability, tool_call, context)
172
+ execution = @tools.create(
173
+ run_id: run.id,
174
+ provider_tool_call_id: tool_call.id,
175
+ batch_id: batch_id,
176
+ batch_position: position,
177
+ capability_name: tool_call.name,
178
+ capability_policy_version: definition.capability_policy_version,
179
+ arguments: tool_call.arguments,
180
+ resource_keys: policy.resource_keys
181
+ )
182
+ PreparedTool.new(
183
+ tool_call: tool_call,
184
+ execution: execution,
185
+ policy: policy,
186
+ policy_error: policy_error
187
+ )
188
+ end
189
+ publish_event(@events.append_run_event(
190
+ run_id: run.id,
191
+ kind: "tool_batch_started",
192
+ data: { batch_id: batch_id, tool_execution_ids: prepared.map { |item| item.execution.id } }
193
+ ))
194
+
195
+ results = Array.new(prepared.size)
196
+ parallel_wave = []
197
+ prepared.each_with_index do |item, position|
198
+ if item.policy.parallel_safe
199
+ parallel_wave << [item, position]
200
+ else
201
+ execute_parallel_wave(parallel_wave, results, run, definition, context)
202
+ parallel_wave = []
203
+ results[position] = execute_tool(run, definition, item, context)
204
+ end
205
+ end
206
+ execute_parallel_wave(parallel_wave, results, run, definition, context)
207
+ results
208
+ end
209
+
210
+ def execute_parallel_wave(wave, results, run, definition, context)
211
+ return if wave.empty?
212
+
213
+ threads = wave.map do |item, position|
214
+ Thread.new do
215
+ Thread.current.report_on_exception = false
216
+ [position, execute_tool(run, definition, item, context)]
217
+ end
218
+ end
219
+ errors = []
220
+ threads.each do |thread|
221
+ begin
222
+ position, result = thread.value
223
+ results[position] = result
224
+ rescue => e
225
+ errors << e
226
+ end
227
+ end
228
+ raise errors.first if errors.any?
229
+ end
230
+
231
+ def execute_tool(run, definition, prepared, context)
232
+ tool_call = prepared.tool_call
233
+ execution = prepared.execution
234
+ context.check_cancelled!
235
+ tool_context = context.for_tool_call(tool_call)
236
+ raise prepared.policy_error if prepared.policy_error
237
+
238
+ validate_capability!(definition, tool_call.name)
239
+ capability = @runtime.registry.get(tool_call.name)
240
+ raise CapabilityError, "Unknown capability: #{tool_call.name}" unless capability
241
+
242
+ invoke = lambda do
243
+ with_resource_locks(prepared.policy, tool_context) do
244
+ @tools.transition(execution.id, status: "running")
245
+ publish_event(@events.append_run_event(
246
+ run_id: run.id,
247
+ kind: "tool_started",
248
+ data: { tool_execution_id: execution.id, name: tool_call.name }
249
+ ))
250
+ @runtime.bus.publish(from: run.po_name, to: tool_call.name, message: tool_call.arguments)
251
+ if capability.is_a?(PromptObject)
252
+ execute_delegation(run, capability, tool_call, tool_context)
253
+ else
254
+ capability.receive(tool_call.arguments, context: tool_context)
255
+ end
256
+ end
257
+ end
258
+ result = if capability.is_a?(PromptObject)
259
+ invoke.call
260
+ else
261
+ with_workload_permit(prepared.policy, &invoke)
262
+ end
263
+ tool_context.check_cancelled!
264
+ content = serialize_tool_content(result)
265
+ @tools.transition(execution.id, status: "completed", result: result)
266
+ publish_event(@events.append_run_event(
267
+ run_id: run.id,
268
+ kind: "tool_completed",
269
+ data: { tool_execution_id: execution.id, name: tool_call.name }
270
+ ))
271
+ @runtime.bus.publish(from: tool_call.name, to: run.po_name, message: result)
272
+ { tool_call_id: tool_call.id, name: tool_call.name, content: content }
273
+ rescue CancelledError => e
274
+ @tools.transition(execution.id, status: "cancelled", cancellation_reason: e.message)
275
+ publish_event(@events.append_run_event(
276
+ run_id: run.id,
277
+ kind: "tool_cancelled",
278
+ data: { tool_execution_id: execution.id, name: tool_call.name }
279
+ ))
280
+ raise
281
+ rescue ResourceLockManager::LockTimeoutError => e
282
+ @tools.transition(execution.id, status: "timed_out", error: e)
283
+ publish_event(@events.append_run_event(
284
+ run_id: run.id,
285
+ kind: "tool_failed",
286
+ data: { tool_execution_id: execution.id, name: tool_call.name, error: error_data(e) }
287
+ ))
288
+ { tool_call_id: tool_call.id, name: tool_call.name, content: "Error: #{e.message}" }
289
+ rescue => e
290
+ @tools.transition(execution.id, status: "failed", error: e)
291
+ publish_event(@events.append_run_event(
292
+ run_id: run.id,
293
+ kind: "tool_failed",
294
+ data: { tool_execution_id: execution.id, name: tool_call.name, error: error_data(e) }
295
+ ))
296
+ { tool_call_id: tool_call.id, name: tool_call.name, content: "Error: #{e.message}" }
297
+ end
298
+
299
+ def resolve_policy(capability, tool_call, context)
300
+ unless capability
301
+ fallback = Policy.new(parallel_safe: false, side_effect: :exclusive)
302
+ return [fallback.resolve(arguments: tool_call.arguments, context: context, capability: nil), nil]
303
+ end
304
+
305
+ [capability.resolved_execution_policy(arguments: tool_call.arguments, context: context), nil]
306
+ rescue => e
307
+ fallback = Policy.new(parallel_safe: false, side_effect: :exclusive)
308
+ [fallback.resolve(arguments: tool_call.arguments, context: context, capability: capability), e]
309
+ end
310
+
311
+ def with_workload_permit(policy, &block)
312
+ permits = policy.workload == :cpu ? @cpu_permits : @tool_permits
313
+ permits ? permits.with(&block) : block.call
314
+ end
315
+
316
+ def with_resource_locks(policy, context, &block)
317
+ return block.call unless @resource_lock_manager
318
+
319
+ @resource_lock_manager.with(
320
+ policy.resource_keys,
321
+ cancellation_token: context.cancellation_token,
322
+ &block
323
+ )
324
+ end
325
+
326
+ def execute_delegation(parent_run, target_po, tool_call, context)
327
+ definition = @runtime.execution_definition_for(target_po)
328
+ child_thread = nil
329
+ child_run = nil
330
+ @runtime.with_child_run_admission(parent_run) do |depth|
331
+ child_thread = @threads.create(
332
+ workspace_session_id: parent_run.workspace_session_id,
333
+ po_name: target_po.name,
334
+ thread_type: "delegation",
335
+ parent_thread_id: parent_run.thread_id,
336
+ parent_run_id: parent_run.id,
337
+ retention_policy: "full"
338
+ )
339
+ child_run = @runs.enqueue(
340
+ workspace_session_id: parent_run.workspace_session_id,
341
+ thread_id: child_thread.id,
342
+ po_name: target_po.name,
343
+ input_content: delegation_content(parent_run, target_po, tool_call.arguments),
344
+ definition_version: definition.version,
345
+ source_type: "delegation",
346
+ source_name: parent_run.po_name,
347
+ parent_run_id: parent_run.id,
348
+ priority: parent_run.priority,
349
+ metadata: {
350
+ delegation_depth: depth,
351
+ deadline: context.deadline&.iso8601(6)
352
+ }.compact
353
+ )
354
+ end
355
+ @definitions.attach(run_id: child_run.id, definition: definition)
356
+ @runtime.publish_workspace_snapshot(parent_run.workspace_session_id)
357
+ publish_event(@events.append_run_event(
358
+ run_id: child_run.id,
359
+ kind: "run_queued",
360
+ data: { source_type: child_run.source_type, turn_ordinal: child_run.turn_ordinal }
361
+ ))
362
+ publish_event(@events.append_run_event(
363
+ run_id: parent_run.id,
364
+ kind: "delegation_started",
365
+ data: { child_run_id: child_run.id, child_thread_id: child_thread.id, target_po: target_po.name }
366
+ ))
367
+ @runs.transition(parent_run.id, status: "waiting")
368
+ publish_event(@events.append_run_event(
369
+ run_id: parent_run.id,
370
+ kind: "run_waiting",
371
+ data: { child_run_id: child_run.id, reason: "awaited delegation" }
372
+ ))
373
+ response = execute(child_run.id, cancellation_token: context.cancellation_token, tui_mode: context.tui_mode)
374
+ publish_event(@events.append_run_event(
375
+ run_id: parent_run.id,
376
+ kind: "delegation_completed",
377
+ data: { child_run_id: child_run.id, child_thread_id: child_thread.id, target_po: target_po.name }
378
+ ))
379
+ response
380
+ ensure
381
+ @runtime.publish_thread_snapshot(child_thread.id, po_name: target_po.name) if child_thread
382
+ @runtime.publish_workspace_snapshot(parent_run.workspace_session_id)
383
+ current = @runs.get(parent_run.id)
384
+ @runs.transition(parent_run.id, status: "running") if current&.status == "waiting"
385
+ end
386
+
387
+ def delegation_content(parent_run, target_po, arguments)
388
+ content = if arguments.is_a?(Hash)
389
+ arguments[:message] || arguments["message"] || arguments.to_s
390
+ else
391
+ arguments.to_s
392
+ end
393
+ <<~MESSAGE.strip
394
+ ---
395
+ [Delegation Context]
396
+ Called by: #{parent_run.po_name}
397
+ Target: #{target_po.name}
398
+ ---
399
+
400
+ #{content}
401
+ MESSAGE
402
+ end
403
+
404
+ def run_deadline(run)
405
+ value = run.metadata[:deadline] || run.metadata["deadline"]
406
+ value && Time.iso8601(value)
407
+ end
408
+
409
+ def persist_tool_results(run, results)
410
+ @messages.append(
411
+ thread_id: run.thread_id,
412
+ run_id: run.id,
413
+ turn_ordinal: run.turn_ordinal,
414
+ role: :tool,
415
+ tool_results: results
416
+ )
417
+ @runtime.publish_thread_snapshot(run.thread_id, po_name: run.po_name)
418
+ end
419
+
420
+ def complete_run(run, response)
421
+ @messages.append(
422
+ thread_id: run.thread_id,
423
+ run_id: run.id,
424
+ turn_ordinal: run.turn_ordinal,
425
+ role: :assistant,
426
+ content: response.content,
427
+ usage: response.usage
428
+ )
429
+ @runs.transition(
430
+ run.id,
431
+ status: "completed",
432
+ final_response: response.content,
433
+ usage: response.usage
434
+ )
435
+ publish_event(@events.append_run_event(run_id: run.id, kind: "run_completed"))
436
+ @runtime.enforce_retention
437
+ response.content
438
+ end
439
+
440
+ def cancel_run(run_id, reason)
441
+ run = @runs.get(run_id)
442
+ return unless run && %w[queued running waiting].include?(run.status)
443
+
444
+ @runs.transition(run_id, status: "cancelled", cancellation_reason: reason)
445
+ publish_event(@events.append_run_event(run_id: run_id, kind: "run_cancelled", data: { reason: reason }))
446
+ @runtime.enforce_retention
447
+ end
448
+
449
+ def fail_run(run_id, error)
450
+ run = @runs.get(run_id)
451
+ return unless run && %w[queued running waiting].include?(run.status)
452
+
453
+ @runs.transition(run_id, status: "failed", error: error)
454
+ publish_event(@events.append_run_event(
455
+ run_id: run_id,
456
+ kind: "run_failed",
457
+ data: { error: error_data(error) }
458
+ ))
459
+ @runtime.enforce_retention
460
+ end
461
+
462
+ def history_for(run)
463
+ @messages.list(thread_id: run.thread_id, through_turn: run.turn_ordinal, limit: 10_000).map do |message|
464
+ history = { role: message[:role], content: message[:content] }
465
+ history[:from] = message[:from_po] if message[:from_po]
466
+ history[:tool_calls] = message[:tool_calls] if message[:tool_calls]
467
+ history[:results] = message[:tool_results] if message[:tool_results]
468
+ history
469
+ end
470
+ end
471
+
472
+ def validate_capability!(definition, capability_name)
473
+ declared = definition.config["capabilities"] || definition.config[:capabilities] || []
474
+ allowed = declared + UNIVERSAL_CAPABILITIES
475
+ return if allowed.include?(capability_name)
476
+
477
+ raise CapabilityError, "Capability '#{capability_name}' is not available"
478
+ end
479
+
480
+ def serialize_tool_content(result)
481
+ return result if result.is_a?(String)
482
+
483
+ JSON.generate(result)
484
+ rescue JSON::GeneratorError
485
+ result.to_s
486
+ end
487
+
488
+ def error_data(error)
489
+ { class: error.class.name, message: error.message }
490
+ end
491
+
492
+ def publish_event(event)
493
+ @event_publisher&.call(event)
494
+ event
495
+ end
496
+ end
497
+ end
498
+ end