prompt_objects 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +126 -0
  3. data/README.md +93 -22
  4. data/exe/poop +5 -0
  5. data/exe/prompt_objects +1 -1108
  6. data/exe/prompt_objects_mcp +5 -3
  7. data/lib/prompt_objects/capability.rb +33 -0
  8. data/lib/prompt_objects/cli/diagnostics_command.rb +69 -0
  9. data/lib/prompt_objects/cli/env_command.rb +435 -0
  10. data/lib/prompt_objects/cli/events_command.rb +145 -0
  11. data/lib/prompt_objects/cli/explore_command.rb +155 -0
  12. data/lib/prompt_objects/cli/helpers.rb +91 -0
  13. data/lib/prompt_objects/cli/message_command.rb +216 -0
  14. data/lib/prompt_objects/cli/repl_command.rb +366 -0
  15. data/lib/prompt_objects/cli/serve_command.rb +162 -0
  16. data/lib/prompt_objects/cli.rb +64 -422
  17. data/lib/prompt_objects/connectors/base.rb +16 -29
  18. data/lib/prompt_objects/connectors/mcp.rb +82 -98
  19. data/lib/prompt_objects/domain/records.rb +45 -0
  20. data/lib/prompt_objects/environment/exporter.rb +5 -4
  21. data/lib/prompt_objects/environment/git.rb +21 -1
  22. data/lib/prompt_objects/environment/importer.rb +5 -4
  23. data/lib/prompt_objects/environment/manager.rb +44 -6
  24. data/lib/prompt_objects/environment/manifest.rb +26 -4
  25. data/lib/prompt_objects/environment.rb +1337 -20
  26. data/lib/prompt_objects/execution/blocking_executor.rb +88 -0
  27. data/lib/prompt_objects/execution/context.rb +137 -0
  28. data/lib/prompt_objects/execution/contracts.rb +59 -0
  29. data/lib/prompt_objects/execution/permit_pool.rb +49 -0
  30. data/lib/prompt_objects/execution/policy.rb +47 -0
  31. data/lib/prompt_objects/execution/resource_lock_manager.rb +63 -0
  32. data/lib/prompt_objects/execution/runner.rb +498 -0
  33. data/lib/prompt_objects/execution/scheduler.rb +325 -0
  34. data/lib/prompt_objects/llm/factory.rb +4 -4
  35. data/lib/prompt_objects/llm/gemini_adapter.rb +25 -8
  36. data/lib/prompt_objects/llm/openai_adapter.rb +9 -1
  37. data/lib/prompt_objects/llm/pricing.rb +9 -2
  38. data/lib/prompt_objects/llm/response.rb +9 -4
  39. data/lib/prompt_objects/loader.rb +28 -1
  40. data/lib/prompt_objects/mcp/server.rb +6 -3
  41. data/lib/prompt_objects/mcp/tools/get_conversation.rb +15 -11
  42. data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +2 -2
  43. data/lib/prompt_objects/mcp/tools/respond_to_request.rb +3 -3
  44. data/lib/prompt_objects/mcp/tools/send_message.rb +42 -32
  45. data/lib/prompt_objects/operations/diagnostics.rb +115 -0
  46. data/lib/prompt_objects/operations/retention.rb +75 -0
  47. data/lib/prompt_objects/operations/run_trace_builder.rb +152 -0
  48. data/lib/prompt_objects/primitives/http_get.rb +2 -0
  49. data/lib/prompt_objects/primitives/list_files.rb +2 -0
  50. data/lib/prompt_objects/primitives/read_file.rb +2 -0
  51. data/lib/prompt_objects/primitives/think.rb +36 -0
  52. data/lib/prompt_objects/primitives/write_file.rb +9 -0
  53. data/lib/prompt_objects/prompt_object.rb +171 -519
  54. data/lib/prompt_objects/repositories/artifact_repository.rb +109 -0
  55. data/lib/prompt_objects/repositories/base_repository.rb +37 -0
  56. data/lib/prompt_objects/repositories/definition_repository.rb +36 -0
  57. data/lib/prompt_objects/repositories/env_data_repository.rb +103 -0
  58. data/lib/prompt_objects/repositories/event_repository.rb +105 -0
  59. data/lib/prompt_objects/repositories/message_repository.rb +73 -0
  60. data/lib/prompt_objects/repositories/run_repository.rb +254 -0
  61. data/lib/prompt_objects/repositories/thread_repository.rb +87 -0
  62. data/lib/prompt_objects/repositories/tool_execution_repository.rb +88 -0
  63. data/lib/prompt_objects/repositories/workspace_session_repository.rb +61 -0
  64. data/lib/prompt_objects/server/api/routes.rb +549 -53
  65. data/lib/prompt_objects/server/app.rb +133 -14
  66. data/lib/prompt_objects/server/file_watcher.rb +9 -0
  67. data/lib/prompt_objects/server/public/assets/index-BjKlODDD.js +90 -0
  68. data/lib/prompt_objects/server/public/assets/index-w3fpIb5c.css +1 -0
  69. data/lib/prompt_objects/server/public/index.html +2 -2
  70. data/lib/prompt_objects/server/websocket_handler.rb +202 -227
  71. data/lib/prompt_objects/server.rb +170 -1
  72. data/lib/prompt_objects/service.rb +129 -0
  73. data/lib/prompt_objects/session/store.rb +600 -13
  74. data/lib/prompt_objects/universal/add_capability.rb +1 -1
  75. data/lib/prompt_objects/universal/create_capability.rb +4 -2
  76. data/lib/prompt_objects/universal/delete_env_data.rb +23 -15
  77. data/lib/prompt_objects/universal/delete_primitive.rb +1 -1
  78. data/lib/prompt_objects/universal/dispatch_po.rb +54 -0
  79. data/lib/prompt_objects/universal/get_env_data.rb +8 -14
  80. data/lib/prompt_objects/universal/get_run_status.rb +30 -0
  81. data/lib/prompt_objects/universal/join_runs.rb +35 -0
  82. data/lib/prompt_objects/universal/list_env_data.rb +10 -14
  83. data/lib/prompt_objects/universal/modify_primitive.rb +1 -1
  84. data/lib/prompt_objects/universal/render_artifact.rb +91 -0
  85. data/lib/prompt_objects/universal/request_primitive.rb +1 -1
  86. data/lib/prompt_objects/universal/{store_env_data.rb → set_env_data.rb} +34 -20
  87. data/lib/prompt_objects/version.rb +5 -0
  88. data/lib/prompt_objects.rb +33 -8
  89. data/objects/coordinator.md +8 -4
  90. data/templates/arc-agi-1/objects/data_manager.md +2 -2
  91. data/templates/arc-agi-1/objects/observer.md +2 -2
  92. data/templates/arc-agi-1/objects/solver.md +6 -5
  93. data/templates/arc-agi-1/objects/verifier.md +2 -2
  94. data/templates/arc-agi-3/manifest.yml +19 -0
  95. data/templates/arc-agi-3/objects/analyst.md +79 -0
  96. data/templates/arc-agi-3/objects/player.md +76 -0
  97. data/templates/arc-agi-3/objects/strategist.md +97 -0
  98. data/templates/arc-agi-3/primitives/action_log.rb +133 -0
  99. data/templates/arc-agi-3/primitives/arc3_api.rb +223 -0
  100. data/templates/arc-agi-3/primitives/arc3_scorecard.rb +131 -0
  101. data/templates/arc-agi-3/primitives/frame_diff.rb +143 -0
  102. data/templates/arc-agi-3/primitives/frame_info.rb +96 -0
  103. data/templates/arc-agi-3/primitives/render_frame.rb +140 -0
  104. data/templates/basic/objects/basic.md +11 -3
  105. data/templates/chiptune/extras/sonic_pi_boot.rb +220 -0
  106. data/templates/chiptune/manifest.yml +20 -0
  107. data/templates/chiptune/objects/conductor.md +184 -0
  108. data/templates/chiptune/objects/sonic_pi.md +65 -0
  109. data/templates/chiptune/objects/voice_bass.md +44 -0
  110. data/templates/chiptune/objects/voice_lead.md +44 -0
  111. data/templates/chiptune/objects/voice_pad.md +43 -0
  112. data/templates/chiptune/objects/voice_perc.md +45 -0
  113. data/templates/chiptune/primitives/play_note.rb +59 -0
  114. data/templates/chiptune/primitives/play_sequence.rb +55 -0
  115. data/templates/chiptune/primitives/query_sonic_pi.rb +141 -0
  116. data/templates/chiptune/primitives/set_synth.rb +57 -0
  117. data/templates/chiptune/primitives/set_tempo.rb +45 -0
  118. data/templates/chiptune/primitives/stop_voice.rb +46 -0
  119. data/templates/chiptune/primitives/support/osc_bridge.rb +33 -0
  120. data/templates/chiptune/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  121. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  122. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  123. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  124. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  125. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  126. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  127. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  128. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  129. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  130. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  131. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  132. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  133. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  134. data/templates/developer/objects/code_reviewer.md +1 -1
  135. data/templates/developer/objects/coordinator.md +16 -3
  136. data/templates/developer/objects/debugger.md +3 -3
  137. data/templates/empty/objects/assistant.md +10 -5
  138. data/templates/minimal/objects/assistant.md +10 -5
  139. data/templates/music/extras/sonic_pi_boot.rb +220 -0
  140. data/templates/music/manifest.yml +16 -0
  141. data/templates/music/objects/conductor.md +154 -0
  142. data/templates/music/objects/sonic_pi.md +65 -0
  143. data/templates/music/primitives/play_note.rb +55 -0
  144. data/templates/music/primitives/play_sequence.rb +51 -0
  145. data/templates/music/primitives/query_sonic_pi.rb +139 -0
  146. data/templates/music/primitives/set_synth.rb +53 -0
  147. data/templates/music/primitives/set_tempo.rb +43 -0
  148. data/templates/music/primitives/stop_voice.rb +44 -0
  149. data/templates/music/primitives/support/osc_bridge.rb +33 -0
  150. data/templates/music/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  151. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  152. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  153. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  154. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  155. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  156. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  157. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  158. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  159. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  160. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  161. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  162. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  163. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  164. data/templates/music-beginner/README.md +100 -0
  165. data/templates/music-beginner/extras/README.md +27 -0
  166. data/templates/music-beginner/extras/sonic_pi_boot.rb +297 -0
  167. data/templates/music-beginner/manifest.yml +25 -0
  168. data/templates/music-beginner/objects/conductor.md +222 -0
  169. data/templates/music-beginner/objects/engine_setup.md +54 -0
  170. data/templates/music-beginner/objects/voice_bass.md +43 -0
  171. data/templates/music-beginner/objects/voice_keys.md +48 -0
  172. data/templates/music-beginner/objects/voice_pad.md +46 -0
  173. data/templates/music-beginner/objects/voice_percussion.md +45 -0
  174. data/templates/music-beginner/objects/voice_texture.md +37 -0
  175. data/templates/music-beginner/primitives/play_pattern.rb +118 -0
  176. data/templates/music-beginner/primitives/query_music_engine.rb +94 -0
  177. data/templates/music-beginner/primitives/set_instrument.rb +47 -0
  178. data/templates/music-beginner/primitives/set_tempo.rb +43 -0
  179. data/templates/music-beginner/primitives/set_voice_level.rb +46 -0
  180. data/templates/music-beginner/primitives/stop_voice.rb +37 -0
  181. data/templates/music-beginner/primitives/support/osc_bridge.rb +25 -0
  182. data/templates/music-beginner/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  183. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  184. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  185. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  186. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  187. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  188. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  189. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  190. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  191. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  192. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  193. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  194. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  195. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  196. data/templates/music-pro/extras/AUDITION.md +100 -0
  197. data/templates/music-pro/extras/sonic_pi_boot.rb +546 -0
  198. data/templates/music-pro/manifest.yml +19 -0
  199. data/templates/music-pro/objects/conductor.md +126 -0
  200. data/templates/music-pro/objects/sonic_pi.md +35 -0
  201. data/templates/music-pro/objects/voice_harmony.md +48 -0
  202. data/templates/music-pro/objects/voice_rhythm.md +43 -0
  203. data/templates/music-pro/objects/voice_texture.md +42 -0
  204. data/templates/music-pro/primitives/define_patch.rb +46 -0
  205. data/templates/music-pro/primitives/query_music_engine.rb +69 -0
  206. data/templates/music-pro/primitives/set_live_pattern.rb +70 -0
  207. data/templates/music-pro/primitives/set_tempo.rb +31 -0
  208. data/templates/music-pro/primitives/stop_voice.rb +31 -0
  209. data/templates/music-pro/primitives/support/music_protocol.rb +136 -0
  210. data/templates/music-pro/primitives/support/osc_bridge.rb +33 -0
  211. data/templates/music-pro/primitives/trigger_event.rb +44 -0
  212. data/templates/music-pro/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  213. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  214. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  215. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  216. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  217. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  218. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  219. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  220. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  221. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  222. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  223. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  224. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  225. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  226. data/templates/writer/objects/coordinator.md +14 -3
  227. data/templates/writer/objects/editor.md +1 -1
  228. data/templates/writer/objects/researcher.md +2 -2
  229. data/tools/thread-explorer.html +1 -1
  230. metadata +172 -65
  231. data/CLAUDE.md +0 -177
  232. data/Gemfile +0 -10
  233. data/Rakefile +0 -27
  234. data/frontend/.gitignore +0 -3
  235. data/frontend/index.html +0 -17
  236. data/frontend/package-lock.json +0 -4540
  237. data/frontend/package.json +0 -36
  238. data/frontend/postcss.config.js +0 -6
  239. data/frontend/src/App.tsx +0 -104
  240. data/frontend/src/canvas/CanvasView.tsx +0 -113
  241. data/frontend/src/canvas/ForceLayout.ts +0 -115
  242. data/frontend/src/canvas/SceneManager.ts +0 -587
  243. data/frontend/src/canvas/canvasStore.ts +0 -47
  244. data/frontend/src/canvas/constants.ts +0 -95
  245. data/frontend/src/canvas/controls/CameraControls.ts +0 -98
  246. data/frontend/src/canvas/edges/MessageArc.ts +0 -149
  247. data/frontend/src/canvas/inspector/InspectorPanel.tsx +0 -31
  248. data/frontend/src/canvas/inspector/POInspector.tsx +0 -262
  249. data/frontend/src/canvas/inspector/ToolCallInspector.tsx +0 -67
  250. data/frontend/src/canvas/nodes/PONode.ts +0 -249
  251. data/frontend/src/canvas/nodes/ToolCallNode.ts +0 -116
  252. data/frontend/src/canvas/types.ts +0 -24
  253. data/frontend/src/components/ContextMenu.tsx +0 -68
  254. data/frontend/src/components/EnvDataPane.tsx +0 -69
  255. data/frontend/src/components/Inspector.tsx +0 -263
  256. data/frontend/src/components/MarkdownMessage.tsx +0 -155
  257. data/frontend/src/components/MethodList.tsx +0 -90
  258. data/frontend/src/components/ModelSelector.tsx +0 -115
  259. data/frontend/src/components/NotificationPanel.tsx +0 -130
  260. data/frontend/src/components/ObjectList.tsx +0 -78
  261. data/frontend/src/components/PaneSlot.tsx +0 -30
  262. data/frontend/src/components/SourcePane.tsx +0 -202
  263. data/frontend/src/components/SystemBar.tsx +0 -74
  264. data/frontend/src/components/Transcript.tsx +0 -76
  265. data/frontend/src/components/UsagePanel.tsx +0 -105
  266. data/frontend/src/components/Workspace.tsx +0 -260
  267. data/frontend/src/components/index.ts +0 -12
  268. data/frontend/src/hooks/useResize.ts +0 -55
  269. data/frontend/src/hooks/useWebSocket.ts +0 -559
  270. data/frontend/src/index.css +0 -102
  271. data/frontend/src/main.tsx +0 -10
  272. data/frontend/src/store/index.ts +0 -311
  273. data/frontend/src/types/index.ts +0 -176
  274. data/frontend/tailwind.config.js +0 -44
  275. data/frontend/tsconfig.json +0 -30
  276. data/frontend/vite.config.ts +0 -29
  277. data/lib/prompt_objects/server/public/assets/index-DEPawnfZ.js +0 -4345
  278. data/lib/prompt_objects/server/public/assets/index-oMrRce1m.css +0 -1
  279. data/lib/prompt_objects/universal/add_primitive.rb +0 -118
  280. data/lib/prompt_objects/universal/ask_human.rb +0 -109
  281. data/lib/prompt_objects/universal/create_primitive.rb +0 -174
  282. data/lib/prompt_objects/universal/list_primitives.rb +0 -145
  283. data/lib/prompt_objects/universal/think.rb +0 -41
  284. data/lib/prompt_objects/universal/update_env_data.rb +0 -88
  285. data/prompt_objects.gemspec +0 -50
  286. data/templates/basic/.gitignore +0 -2
  287. data/templates/developer/.gitignore +0 -5
  288. data/templates/empty/.gitignore +0 -5
  289. data/templates/empty/objects/.gitkeep +0 -0
  290. data/templates/minimal/.gitignore +0 -5
  291. data/templates/writer/.gitignore +0 -5
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "json"
4
4
  require "rack"
5
+ require "yaml"
5
6
 
6
7
  module PromptObjects
7
8
  module Server
@@ -11,11 +12,27 @@ module PromptObjects
11
12
  class Routes
12
13
  def initialize(runtime)
13
14
  @runtime = runtime
15
+ if runtime.session_store
16
+ @workspace_repository = Repositories::WorkspaceSessionRepository.new(runtime.session_store)
17
+ @thread_repository = Repositories::ThreadRepository.new(runtime.session_store)
18
+ @run_repository = Repositories::RunRepository.new(runtime.session_store)
19
+ @message_repository = Repositories::MessageRepository.new(runtime.session_store)
20
+ @event_repository = Repositories::EventRepository.new(runtime.session_store)
21
+ @artifact_repository = Repositories::ArtifactRepository.new(runtime.session_store)
22
+ @env_data_repository = Repositories::EnvDataRepository.new(runtime.session_store)
23
+ @run_trace_builder = Operations::RunTraceBuilder.new(runtime.session_store)
24
+ end
14
25
  end
15
26
 
16
27
  def call(env)
17
28
  request = Rack::Request.new(env)
18
- path = request.path_info.sub("/api", "")
29
+ # Falcon delivers PATH_INFO as ASCII-8BIT. SQLite binds binary strings
30
+ # as BLOBs, which never compare equal to TEXT columns — so any keyed
31
+ # lookup from a URL path (artifacts, sessions) would silently miss.
32
+ # Normalize to UTF-8 before routing.
33
+ path = request.path_info.dup.force_encoding(Encoding::UTF_8)
34
+ path = path.scrub unless path.valid_encoding?
35
+ path = path.sub("/api", "")
19
36
 
20
37
  response = route(request, path)
21
38
  json_response(response)
@@ -42,6 +59,8 @@ module PromptObjects
42
59
  case path
43
60
  when "/environment"
44
61
  get_environment
62
+ when "/diagnostics"
63
+ @runtime.operational_diagnostics
45
64
  when "/prompt_objects"
46
65
  list_prompt_objects
47
66
  when "/primitives"
@@ -56,8 +75,36 @@ module PromptObjects
56
75
  list_sessions($1)
57
76
  when %r{^/prompt_objects/([^/]+)$}
58
77
  get_prompt_object($1)
78
+ when "/sessions"
79
+ get_environment_sessions
80
+ when "/workspaces"
81
+ list_workspaces
82
+ when %r{^/workspaces/([^/]+)/snapshot$}
83
+ get_workspace_snapshot($1)
84
+ when %r{^/workspaces/([^/]+)/threads$}
85
+ list_runtime_threads($1, _request)
86
+ when %r{^/workspaces/([^/]+)/runs$}
87
+ list_runtime_runs($1, _request)
88
+ when %r{^/workspaces/([^/]+)/events$}
89
+ list_workspace_events($1, _request)
90
+ when %r{^/workspaces/([^/]+)/artifacts$}
91
+ list_runtime_artifacts($1)
92
+ when %r{^/workspaces/([^/]+)/env_data$}
93
+ list_runtime_env_data($1)
94
+ when %r{^/threads/([^/]+)/messages$}
95
+ list_runtime_messages($1, _request)
96
+ when %r{^/threads/([^/]+)$}
97
+ get_runtime_thread($1)
98
+ when %r{^/runs/([^/]+)/events$}
99
+ list_run_events($1, _request)
100
+ when %r{^/runs/([^/]+)/trace$}
101
+ get_run_trace($1)
102
+ when %r{^/runs/([^/]+)$}
103
+ get_runtime_run($1)
59
104
  when %r{^/events/session/([^/]+)$}
60
105
  get_session_events($1)
106
+ when %r{^/artifacts/([^/]+)$}
107
+ get_artifact($1)
61
108
  when %r{^/sessions/([^/]+)/env_data$}
62
109
  get_session_env_data($1)
63
110
  when %r{^/sessions/([^/]+)/usage$}
@@ -71,12 +118,32 @@ module PromptObjects
71
118
 
72
119
  def route_post(request, path)
73
120
  case path
121
+ when "/messages"
122
+ inject_message(request.body.read)
74
123
  when "/prompt_objects"
75
124
  create_prompt_object(request.body.read)
76
125
  when %r{^/prompt_objects/([^/]+)/message$}
77
126
  send_message($1, request.body.read)
78
127
  when %r{^/prompt_objects/([^/]+)/sessions$}
79
128
  create_session($1, request.body.read)
129
+ when "/sessions"
130
+ create_environment_session_endpoint(request.body.read)
131
+ when %r{^/sessions/([^/]+)/activate$}
132
+ activate_session($1)
133
+ when %r{^/sessions/([^/]+)/rename$}
134
+ rename_session_endpoint($1, request.body.read)
135
+ when %r{^/sessions/([^/]+)/delete$}
136
+ delete_session_endpoint($1)
137
+ when %r{^/artifacts/([^/]+)/message$}
138
+ artifact_message($1, request.body.read)
139
+ when %r{^/runs/([^/]+)/cancel$}
140
+ cancel_runtime_run($1, request.body.read)
141
+ when %r{^/workspaces/([^/]+)/threads$}
142
+ create_runtime_thread($1, request.body.read)
143
+ when %r{^/threads/([^/]+)/rename$}
144
+ rename_runtime_thread($1, request.body.read)
145
+ when %r{^/threads/([^/]+)/archive$}
146
+ archive_runtime_thread($1)
80
147
  else
81
148
  { error: "Not found", path: path }
82
149
  end
@@ -124,13 +191,89 @@ module PromptObjects
124
191
  po_full(po)
125
192
  end
126
193
 
194
+ # POST /api/prompt_objects — create a new Prompt-Object from JSON.
195
+ # Body: { name, description, capabilities?, identity?, behavior? }
196
+ # Mirrors the create_capability universal: writes a .md and loads it.
127
197
  def create_prompt_object(body)
128
- # TODO: Implement PO creation via API
129
- { error: "Not implemented" }
198
+ params = JSON.parse(body)
199
+ name = params["name"].to_s
200
+ description = params["description"].to_s
201
+ capabilities = params["capabilities"] || []
202
+ identity = params["identity"] || "You are a helpful prompt object."
203
+ behavior = params["behavior"] || "Help with the request."
204
+
205
+ return { error: "name is required" } if name.empty?
206
+ unless name.match?(/\A[a-z][a-z0-9_]*\z/)
207
+ return { error: "name must be lowercase letters, numbers, and underscores, starting with a letter" }
208
+ end
209
+ return { error: "A capability named '#{name}' already exists" } if @runtime.registry.exists?(name)
210
+ return { error: "No objects directory available" } unless @runtime.objects_dir
211
+
212
+ frontmatter = { "name" => name, "description" => description, "capabilities" => capabilities }
213
+ body_md = <<~MD
214
+ # #{name.split('_').map(&:capitalize).join(' ')}
215
+
216
+ ## Identity
217
+
218
+ #{identity}
219
+
220
+ ## Behavior
221
+
222
+ #{behavior}
223
+ MD
224
+ content = "#{frontmatter.to_yaml}---\n\n#{body_md}"
225
+
226
+ path = File.join(@runtime.objects_dir, "#{name}.md")
227
+ File.write(path, content, encoding: "UTF-8")
228
+ po = @runtime.load_prompt_object(path)
229
+
230
+ po_summary(po)
231
+ rescue JSON::ParserError
232
+ { error: "Invalid JSON body" }
233
+ rescue => e
234
+ { error: e.message }
130
235
  end
131
236
 
132
237
  # === Messages ===
133
238
 
239
+ # POST /api/messages — Universal message injection endpoint.
240
+ # External systems (webhooks, cron, bots) use this to trigger POs.
241
+ # Uses runtime.inject_message — same code path as human/timer messages.
242
+ def inject_message(body)
243
+ params = JSON.parse(body)
244
+ target_po = params["target_po"]
245
+ content = params["content"]
246
+ from_source = params["from_source"] || "api"
247
+
248
+ return { error: "target_po is required" } unless target_po && !target_po.empty?
249
+ return { error: "content is required" } unless content && !content.empty?
250
+
251
+ run, response = @runtime.execute_message_run(
252
+ target_po: target_po,
253
+ content: content,
254
+ from_source: from_source,
255
+ workspace_session_id: params["workspace_session_id"],
256
+ thread_id: params["thread_id"],
257
+ client_request_id: params["client_request_id"],
258
+ deadline: params["deadline"],
259
+ tui_mode: false
260
+ )
261
+ {
262
+ response: response,
263
+ run_id: run.id,
264
+ thread_id: run.thread_id,
265
+ workspace_session_id: run.workspace_session_id,
266
+ status: run.status,
267
+ target_po: target_po,
268
+ from_source: from_source
269
+ }
270
+ rescue JSON::ParserError
271
+ { error: "Invalid JSON body" }
272
+ rescue PromptObjects::Error => e
273
+ { error: e.message }
274
+ end
275
+
276
+ # POST /api/prompt_objects/:name/message — Legacy per-PO message endpoint.
134
277
  def send_message(po_name, body)
135
278
  po = @runtime.registry.get(po_name)
136
279
 
@@ -140,50 +283,76 @@ module PromptObjects
140
283
 
141
284
  params = JSON.parse(body)
142
285
  message = params["message"]
143
- session_id = params["session_id"]
144
286
  new_thread = params["new_thread"]
145
287
 
146
288
  return { error: "Message is required" } unless message && !message.empty?
147
289
 
148
- # Create a new thread if requested
290
+ workspace_id = params["workspace_session_id"] || @runtime.active_session(initiated_by: "api")[:id]
291
+ thread_id = params["thread_id"] || params["session_id"]
149
292
  if new_thread
150
- session_id = po.new_thread
293
+ thread_id = @runtime.create_runtime_thread(
294
+ workspace_session_id: workspace_id,
295
+ po_name: po_name,
296
+ thread_type: "api",
297
+ name: params["name"]
298
+ ).id
151
299
  end
152
-
153
- # Switch to specified session if provided
154
- if session_id && session_id != po.session_id
155
- po.switch_session(session_id)
156
- end
157
-
158
- request_session_id = po.session_id
159
-
160
- # Send the message through the same path as WebSocket
161
- context = @runtime.context(tui_mode: false)
162
- context.current_capability = "human"
163
-
164
- # Log to bus
165
- @runtime.bus.publish(from: "human", to: po.name, message: message, session_id: request_session_id)
166
-
167
- response = po.receive(message, context: context)
168
-
169
- # Log response to bus
170
- @runtime.bus.publish(from: po.name, to: "human", message: response, session_id: request_session_id)
171
-
172
- # Count events for this session
173
- event_count = if @runtime.session_store
174
- @runtime.session_store.get_events(session_id: request_session_id).length
175
- end
176
-
300
+ run, response = @runtime.execute_message_run(
301
+ target_po: po_name,
302
+ content: message,
303
+ from_source: params["from_source"] || "api",
304
+ workspace_session_id: workspace_id,
305
+ thread_id: thread_id,
306
+ client_request_id: params["client_request_id"],
307
+ deadline: params["deadline"],
308
+ tui_mode: false
309
+ )
177
310
  {
178
311
  response: response,
179
- po_name: po.name,
180
- session_id: request_session_id,
181
- event_count: event_count
312
+ po_name: po_name,
313
+ session_id: run.thread_id,
314
+ thread_id: run.thread_id,
315
+ run_id: run.id,
316
+ workspace_session_id: run.workspace_session_id,
317
+ status: run.status,
318
+ event_count: @event_repository.run_events(run_id: run.id).length
182
319
  }
183
320
  rescue JSON::ParserError
184
321
  { error: "Invalid JSON body" }
185
322
  end
186
323
 
324
+ def cancel_runtime_run(run_id, body)
325
+ params = body.to_s.empty? ? {} : JSON.parse(body)
326
+ cancelled = @runtime.cancel_run(run_id, reason: params["reason"] || "api request")
327
+ { run_id: run_id, cancelled: cancelled }
328
+ rescue JSON::ParserError
329
+ { error: "Invalid JSON body" }
330
+ end
331
+
332
+ def create_runtime_thread(workspace_id, body)
333
+ params = JSON.parse(body)
334
+ thread = @runtime.create_runtime_thread(
335
+ workspace_session_id: workspace_id,
336
+ po_name: params["po_name"],
337
+ thread_type: params["thread_type"] || "human",
338
+ name: params["name"]
339
+ )
340
+ serialize_record(thread)
341
+ rescue JSON::ParserError
342
+ { error: "Invalid JSON body" }
343
+ end
344
+
345
+ def rename_runtime_thread(thread_id, body)
346
+ params = JSON.parse(body)
347
+ serialize_record(@runtime.rename_runtime_thread(thread_id, name: params["name"]))
348
+ rescue JSON::ParserError
349
+ { error: "Invalid JSON body" }
350
+ end
351
+
352
+ def archive_runtime_thread(thread_id)
353
+ serialize_record(@runtime.archive_runtime_thread(thread_id))
354
+ end
355
+
187
356
  # === Sessions ===
188
357
 
189
358
  def list_sessions(po_name)
@@ -193,9 +362,12 @@ module PromptObjects
193
362
  return { error: "Not found", name: po_name }
194
363
  end
195
364
 
196
- sessions = po.list_sessions.map do |s|
197
- PromptObject.serialize_session(s)
198
- end
365
+ workspace_id = @runtime.active_session(initiated_by: "api")[:id]
366
+ sessions = @thread_repository.list(
367
+ workspace_session_id: workspace_id,
368
+ po_name: po_name,
369
+ include_archived: true
370
+ ).map { |thread| serialize_record(thread) }
199
371
 
200
372
  { sessions: sessions }
201
373
  end
@@ -207,23 +379,19 @@ module PromptObjects
207
379
  return { error: "Prompt object not found", name: po_name }
208
380
  end
209
381
 
210
- return { error: "No session store" } unless @runtime.session_store
211
-
212
- session = @runtime.session_store.get_session(session_id)
213
-
214
- unless session && session[:po_name] == po_name
382
+ thread = @thread_repository.get(session_id)
383
+ unless thread && thread.po_name == po_name
215
384
  return { error: "Session not found", id: session_id }
216
385
  end
217
-
218
- messages = @runtime.session_store.get_messages(session_id)
386
+ messages = @message_repository.list(thread_id: session_id, limit: 10_000)
219
387
 
220
388
  {
221
- id: session[:id],
222
- name: session[:name],
223
- po_name: session[:po_name],
389
+ id: thread.id,
390
+ name: thread.name,
391
+ po_name: thread.po_name,
224
392
  messages: messages.map { |m| format_message(m) },
225
- created_at: session[:created_at]&.iso8601,
226
- updated_at: session[:updated_at]&.iso8601
393
+ created_at: thread.created_at&.iso8601,
394
+ updated_at: thread.updated_at&.iso8601
227
395
  }
228
396
  end
229
397
 
@@ -237,15 +405,173 @@ module PromptObjects
237
405
  params = body.empty? ? {} : JSON.parse(body)
238
406
  session_name = params["name"]
239
407
 
240
- session_id = po.new_session(name: session_name)
408
+ workspace_id = params["workspace_session_id"] || @runtime.active_session(initiated_by: "api")[:id]
409
+ thread = @runtime.create_runtime_thread(
410
+ workspace_session_id: workspace_id,
411
+ po_name: po_name,
412
+ thread_type: "human",
413
+ name: session_name
414
+ )
241
415
 
242
416
  {
243
417
  success: true,
244
- session_id: session_id,
418
+ session_id: thread.id,
419
+ thread_id: thread.id,
420
+ workspace_session_id: thread.workspace_session_id,
245
421
  name: session_name
246
422
  }
247
423
  end
248
424
 
425
+ # === Run runtime queries ===
426
+
427
+ def list_workspaces
428
+ return no_runtime_store unless @workspace_repository
429
+
430
+ { workspaces: @workspace_repository.list.map { |workspace| serialize_record(workspace) } }
431
+ end
432
+
433
+ def get_workspace_snapshot(id)
434
+ return no_runtime_store unless @workspace_repository
435
+
436
+ workspace = @workspace_repository.get(id)
437
+ return { error: "Workspace not found", id: id } unless workspace
438
+
439
+ {
440
+ version: Execution::Contracts::PROTOCOL_VERSION,
441
+ type: "workspace_snapshot",
442
+ payload: {
443
+ cursor: @event_repository.current_cursor(workspace_session_id: id),
444
+ workspace: serialize_record(workspace),
445
+ threads: @thread_repository.list(
446
+ workspace_session_id: id,
447
+ include_archived: true
448
+ ).map { |thread| serialize_record(thread) },
449
+ runs: @run_repository.list(
450
+ workspace_session_id: id,
451
+ limit: 500
452
+ ).map { |run| serialize_record(run) },
453
+ artifacts: @artifact_repository.list(
454
+ workspace_session_id: id
455
+ ).map { |artifact| serialize_record(artifact) }
456
+ }
457
+ }
458
+ end
459
+
460
+ def list_runtime_threads(workspace_id, request)
461
+ return no_runtime_store unless @thread_repository
462
+ return { error: "Workspace not found", id: workspace_id } unless @workspace_repository.get(workspace_id)
463
+
464
+ threads = @thread_repository.list(
465
+ workspace_session_id: workspace_id,
466
+ po_name: request.params["po_name"],
467
+ include_archived: request.params["include_archived"] == "true"
468
+ )
469
+ { threads: threads.map { |thread| serialize_record(thread) } }
470
+ end
471
+
472
+ def get_runtime_thread(id)
473
+ return no_runtime_store unless @thread_repository
474
+
475
+ thread = @thread_repository.get(id)
476
+ thread ? serialize_record(thread) : { error: "Thread not found", id: id }
477
+ end
478
+
479
+ def list_runtime_messages(thread_id, request)
480
+ return no_runtime_store unless @message_repository
481
+ return { error: "Thread not found", id: thread_id } unless @thread_repository.get(thread_id)
482
+
483
+ after_id = nonnegative_integer(request.params["after_id"])
484
+ limit = bounded_limit(request.params["limit"], maximum: 500)
485
+ messages = @message_repository.list(thread_id: thread_id, after_id: after_id, limit: limit)
486
+ {
487
+ thread_id: thread_id,
488
+ messages: messages.map { |message| serialize_runtime_message(message) },
489
+ next_after_id: messages.last&.dig(:id)
490
+ }
491
+ end
492
+
493
+ def list_runtime_runs(workspace_id, request)
494
+ return no_runtime_store unless @run_repository
495
+ return { error: "Workspace not found", id: workspace_id } unless @workspace_repository.get(workspace_id)
496
+
497
+ statuses = request.params["status"]&.split(",")
498
+ runs = @run_repository.list(
499
+ workspace_session_id: workspace_id,
500
+ thread_id: request.params["thread_id"],
501
+ statuses: statuses,
502
+ limit: bounded_limit(request.params["limit"], maximum: 500)
503
+ )
504
+ { runs: runs.map { |run| serialize_record(run) } }
505
+ rescue ArgumentError => e
506
+ { error: e.message }
507
+ end
508
+
509
+ def get_runtime_run(id)
510
+ return no_runtime_store unless @run_repository
511
+
512
+ run = @run_repository.get(id)
513
+ run ? serialize_record(run) : { error: "Run not found", id: id }
514
+ end
515
+
516
+ def list_run_events(run_id, request)
517
+ return no_runtime_store unless @event_repository
518
+ return { error: "Run not found", id: run_id } unless @run_repository.get(run_id)
519
+
520
+ events = @event_repository.run_events(
521
+ run_id: run_id,
522
+ after_sequence: nonnegative_integer(request.params["after_sequence"])
523
+ )
524
+ { run_id: run_id, events: events.map { |event| serialize_record(event) } }
525
+ end
526
+
527
+ def get_run_trace(run_id)
528
+ return no_runtime_store unless @run_trace_builder
529
+ return { error: "Run not found", id: run_id } unless @run_repository.get(run_id)
530
+
531
+ { trace: serialize_value(@run_trace_builder.build(run_id)) }
532
+ end
533
+
534
+ def list_workspace_events(workspace_id, request)
535
+ return no_runtime_store unless @event_repository
536
+ return { error: "Workspace not found", id: workspace_id } unless @workspace_repository.get(workspace_id)
537
+
538
+ events = @event_repository.workspace_events(
539
+ workspace_session_id: workspace_id,
540
+ after_cursor: nonnegative_integer(request.params["after_cursor"]),
541
+ limit: bounded_limit(request.params["limit"], maximum: 1_000)
542
+ )
543
+ {
544
+ workspace_session_id: workspace_id,
545
+ events: events.map { |event| serialize_value(event) },
546
+ next_cursor: events.last&.dig(:cursor) || nonnegative_integer(request.params["after_cursor"])
547
+ }
548
+ end
549
+
550
+ def list_runtime_artifacts(workspace_id)
551
+ return no_runtime_store unless @artifact_repository
552
+ return { error: "Workspace not found", id: workspace_id } unless @workspace_repository.get(workspace_id)
553
+
554
+ {
555
+ workspace_session_id: workspace_id,
556
+ cursor: @event_repository.current_cursor(workspace_session_id: workspace_id),
557
+ artifacts: @artifact_repository.list(
558
+ workspace_session_id: workspace_id
559
+ ).map { |artifact| serialize_record(artifact) }
560
+ }
561
+ end
562
+
563
+ def list_runtime_env_data(workspace_id)
564
+ return no_runtime_store unless @env_data_repository
565
+ return { error: "Workspace not found", id: workspace_id } unless @workspace_repository.get(workspace_id)
566
+
567
+ {
568
+ workspace_session_id: workspace_id,
569
+ entries: @env_data_repository.list(
570
+ workspace_session_id: workspace_id
571
+ ).map { |entry| serialize_value(entry) }
572
+ }
573
+ end
574
+
249
575
  # === Primitives ===
250
576
 
251
577
  def list_primitives
@@ -316,12 +642,139 @@ module PromptObjects
316
642
  }
317
643
  end
318
644
 
645
+ # === Artifacts ===
646
+
647
+ def get_artifact(key)
648
+ return { error: "No session store" } unless @runtime.session_store
649
+
650
+ artifact = @artifact_repository&.get(key)
651
+ return serialize_record(artifact).merge(rendered_by: artifact.owner_po) if artifact
652
+
653
+ { error: "Artifact not found", key: key }
654
+ rescue => e
655
+ { error: "Artifact lookup failed: #{e.message}", key: key }
656
+ end
657
+
658
+ # POST /api/artifacts/:key/message — BURP (Bidirectional UI Rendering
659
+ # Protocol). Interactive artifacts call PromptObjects.send(...) in the
660
+ # browser, which lands here; the message is injected into the PO that
661
+ # rendered the artifact, with provenance "artifact:<title>".
662
+ MAX_ARTIFACT_MESSAGE_LENGTH = 4_000
663
+
664
+ def artifact_message(key, body)
665
+ params = JSON.parse(body)
666
+ content = params["content"]
667
+
668
+ return { error: "content is required" } unless content.is_a?(String) && !content.strip.empty?
669
+ if content.length > MAX_ARTIFACT_MESSAGE_LENGTH
670
+ return { error: "content too long (max #{MAX_ARTIFACT_MESSAGE_LENGTH} characters)" }
671
+ end
672
+
673
+ artifact = get_artifact(key)
674
+ return artifact if artifact[:error]
675
+
676
+ target_po = artifact[:owner_po] || artifact[:rendered_by]
677
+ unless target_po && @runtime.registry.get(target_po).is_a?(PromptObject)
678
+ return { error: "Rendering PO '#{target_po}' not found for artifact", key: key }
679
+ end
680
+
681
+ from_source = "artifact:#{artifact[:title] || key}"
682
+ # The from_source provenance is UI metadata the LLM never sees — make
683
+ # the interface origin visible in the message itself, so the PO knows
684
+ # to respond by re-rendering the artifact rather than with prose.
685
+ framed_content = %([via your artifact "#{artifact[:title] || key}"] #{content}\n\n) +
686
+ "(The human is looking at the artifact, not this chat — respond by re-rendering " \
687
+ "\"#{artifact[:title] || key}\" with render_artifact in replace mode, updated for this action.)"
688
+ run = @runtime.enqueue_run(
689
+ target_po: target_po,
690
+ content: framed_content,
691
+ from_source: from_source,
692
+ workspace_session_id: artifact[:workspace_session_id],
693
+ thread_id: artifact[:owner_thread_id],
694
+ client_request_id: params["client_request_id"],
695
+ deadline: params["deadline"]
696
+ )
697
+ @runtime.schedule_run(run.id, tui_mode: false)
698
+ {
699
+ queued: true,
700
+ run_id: run.id,
701
+ thread_id: run.thread_id,
702
+ workspace_session_id: run.workspace_session_id,
703
+ target_po: target_po,
704
+ from_source: from_source
705
+ }
706
+ rescue JSON::ParserError
707
+ { error: "Invalid JSON body" }
708
+ rescue PromptObjects::Error => e
709
+ { error: e.message }
710
+ end
711
+
712
+ # === Environment Sessions (shared-context "jams") ===
713
+
714
+ # GET /api/sessions — list environment sessions, with which is active.
715
+ def get_environment_sessions
716
+ return { error: "No session store" } unless @runtime.session_store
717
+
718
+ { sessions: @runtime.environment_sessions.map { |s| serialize_environment_session(s) } }
719
+ end
720
+
721
+ # POST /api/sessions — create a new environment session (becomes active).
722
+ def create_environment_session_endpoint(body)
723
+ return { error: "No session store" } unless @runtime.session_store
724
+
725
+ name = parse_json(body)["name"]
726
+ session = @runtime.create_environment_session(name: name)
727
+ serialize_environment_session(session.merge(active: true))
728
+ end
729
+
730
+ # POST /api/sessions/:id/activate — switch the active environment session.
731
+ def activate_session(id)
732
+ session = @runtime.switch_active_session(id)
733
+ return { error: "Session not found", id: id } unless session
734
+
735
+ serialize_environment_session(session.merge(active: true))
736
+ end
737
+
738
+ # POST /api/sessions/:id/rename — rename an environment session.
739
+ def rename_session_endpoint(id, body)
740
+ name = parse_json(body)["name"]
741
+ return { error: "name is required" } if name.nil? || name.to_s.empty?
742
+
743
+ session = @runtime.rename_session(id, name)
744
+ return { error: "Session not found", id: id } unless session
745
+
746
+ serialize_environment_session(session)
747
+ end
748
+
749
+ # POST /api/sessions/:id/delete — delete an environment session.
750
+ def delete_session_endpoint(id)
751
+ ok = @runtime.delete_environment_session(id)
752
+ ok ? { deleted: id } : { error: "Session not found", id: id }
753
+ end
754
+
755
+ def serialize_environment_session(s)
756
+ {
757
+ id: s[:id],
758
+ name: s[:name],
759
+ active: s[:active] || false,
760
+ message_count: s[:message_count] || 0,
761
+ created_at: s[:created_at]&.iso8601,
762
+ updated_at: s[:updated_at]&.iso8601
763
+ }
764
+ end
765
+
766
+ def parse_json(body)
767
+ JSON.parse(body.to_s.empty? ? "{}" : body)
768
+ rescue JSON::ParserError
769
+ {}
770
+ end
771
+
319
772
  # === Env Data ===
320
773
 
321
774
  def get_session_env_data(session_id)
322
775
  return { error: "No session store" } unless @runtime.session_store
323
776
 
324
- root_thread_id = @runtime.session_store.resolve_root_thread(session_id)
777
+ root_thread_id = @runtime.env_data_scope(session_id)
325
778
  entries = @runtime.session_store.list_env_data_full(root_thread_id: root_thread_id)
326
779
 
327
780
  { root_thread_id: root_thread_id, entries: entries }
@@ -378,6 +831,44 @@ module PromptObjects
378
831
 
379
832
  # === Helpers ===
380
833
 
834
+ def no_runtime_store
835
+ { error: "Run runtime storage is unavailable" }
836
+ end
837
+
838
+ def bounded_limit(value, maximum:)
839
+ requested = value.to_i
840
+ requested = 100 unless requested.positive?
841
+ [requested, maximum].min
842
+ end
843
+
844
+ def nonnegative_integer(value)
845
+ [value.to_i, 0].max
846
+ end
847
+
848
+ def serialize_record(record)
849
+ serialize_value(record.to_h)
850
+ end
851
+
852
+ def serialize_runtime_message(message)
853
+ serialize_value(message).merge(
854
+ id: message[:message_uid] || message[:id].to_s,
855
+ cursor_id: message[:id]
856
+ )
857
+ end
858
+
859
+ def serialize_value(value)
860
+ case value
861
+ when Time
862
+ value.iso8601
863
+ when Hash
864
+ value.to_h { |key, nested| [key, serialize_value(nested)] }
865
+ when Array
866
+ value.map { |nested| serialize_value(nested) }
867
+ else
868
+ value
869
+ end
870
+ end
871
+
381
872
  def po_summary(po)
382
873
  po.to_summary_hash(registry: @runtime.registry)
383
874
  end
@@ -411,7 +902,12 @@ module PromptObjects
411
902
  status,
412
903
  {
413
904
  "content-type" => "application/json",
414
- "access-control-allow-origin" => "*"
905
+ "access-control-allow-origin" => "*",
906
+ # The API is live state — sessions, artifacts, PO status all change
907
+ # constantly. Without this, browsers heuristically cache same-URL
908
+ # GETs, so a refresh after creating a session (or rendering an
909
+ # artifact) re-reads a stale list and the new thing never appears.
910
+ "cache-control" => "no-store, must-revalidate"
415
911
  },
416
912
  [body]
417
913
  ]